Opera8 commited on
Commit
ba5b442
·
verified ·
1 Parent(s): f6275e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +322 -98
app.py CHANGED
@@ -1,111 +1,335 @@
1
  import os
2
- import base64
3
- import logging
4
- import torch
5
- import uvicorn
6
- from fastapi import FastAPI, HTTPException
7
- from pydantic import BaseModel
8
- from typing import List, Optional
9
- from PIL import Image
10
- from io import BytesIO
11
- import spaces
12
- from transformers import AutoModelForMultimodalLM, AutoProcessor
13
 
14
- # --- تنظیمات لاگر ---
15
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - [FastAPI-Gemma] %(message)s')
16
- logger = logging.getLogger(__name__)
 
 
17
 
18
  MODEL_ID = "google/gemma-4-e4b-it"
19
 
20
- # ۱. بارگذاری پردازشگر و مدل در سطح Global
21
- logger.info("Loading Model and Processor...")
22
  processor = AutoProcessor.from_pretrained(MODEL_ID, use_fast=False)
23
  model = AutoModelForMultimodalLM.from_pretrained(MODEL_ID, device_map="auto", dtype=torch.bfloat16)
24
- logger.info("Model Loaded Successfully!")
25
-
26
- # ۲. تعریف تابع GPU در بالاترین سطح (برای شناسایی توسط اسکنر هگینگ‌فیس)
27
- @spaces.GPU(duration=100)
28
- def run_gemma_core_logic(messages, max_new_tokens):
29
- # آماده‌سازی ورودی‌ها
30
- inputs = processor.apply_chat_template(
31
- messages,
32
- tokenize=True,
33
- return_dict=True,
34
- return_tensors="pt",
35
- add_generation_prompt=True
36
- ).to(model.device, dtype=torch.bfloat16)
37
-
38
- # تولید پاسخ بدون ذخیره گرادیان‌ها برای بهینه بودن
39
- with torch.no_grad():
40
- output = model.generate(
41
- **inputs,
42
- max_new_tokens=max_new_tokens,
43
- do_sample=False
44
- )
45
-
46
- # استخراج متن خالص پاسخ
47
- prompt_len = inputs.input_ids.shape[1]
48
- return processor.decode(output[0][prompt_len:], skip_special_tokens=True)
49
-
50
- # ۳. تنظیمات اپلیکیشن FastAPI
51
- app = FastAPI()
52
-
53
- class ChatRequest(BaseModel):
54
- prompt: str
55
- image_base64: Optional[str] = None
56
- max_tokens: int = 1024
57
- system_prompt: str = "شما یک دستیار هوش مصنوعی بسیار باهوش و مودب به زبان فارسی هستید."
58
-
59
- def process_incoming_image(base64_str: str) -> Image.Image:
60
- try:
61
- if "base64," in base64_str:
62
- base64_str = base64_str.split("base64,")[1]
63
- img_data = base64.b64decode(base64_str)
64
- img = Image.open(BytesIO(img_data))
65
-
66
- # بهینه‌سازی ابعاد (Resize) برای جلوگیری از اشباع حافظه GPU
67
- MAX_DIM = 1024
68
- if max(img.size) > MAX_DIM:
69
- ratio = MAX_DIM / float(max(img.size))
70
- new_size = tuple([int(x * ratio) for x in img.size])
71
- img = img.resize(new_size, Image.Resampling.LANCZOS)
72
-
73
- return img.convert("RGB")
74
- except Exception as e:
75
- logger.error(f"Image Decoding Error: {e}")
76
- raise ValueError("فرمت تصویر ارسالی معتبر نیست.")
77
-
78
- @app.post("/chat")
79
- async def chat_api(request: ChatRequest):
80
- logger.info(f"Request: {request.prompt[:30]}...")
81
-
82
- # ساختار پیام‌ها
83
- messages = []
84
- if request.system_prompt:
85
- messages.append({"role": "system", "content": [{"type": "text", "text": request.system_prompt}]})
86
-
87
- user_content = []
88
- if request.image_base64:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  try:
90
- img = process_incoming_image(request.image_base64)
91
- user_content.append({"type": "image", "image": img})
92
- except Exception as e:
93
- return {"status": "error", "message": str(e)}
94
-
95
- user_content.append({"type": "text", "text": request.prompt})
96
- messages.append({"role": "user", "content": user_content})
97
 
 
98
  try:
99
- # فراخوانی تابع GPU
100
- answer = run_gemma_core_logic(messages, request.max_tokens)
101
- return {"status": "success", "response": answer}
102
- except Exception as e:
103
- logger.error(f"Inference Error: {e}")
104
- return {"status": "error", "message": "خطای فنی در استنتاج مدل."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- @app.get("/")
107
- def home():
108
- return {"status": "online", "endpoint": "/chat"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  if __name__ == "__main__":
111
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import os
2
+ from collections.abc import Iterator
3
+ from threading import Thread
 
 
 
 
 
 
 
 
 
4
 
5
+ import gradio as gr
6
+ import spaces
7
+ import torch
8
+ from transformers import AutoModelForMultimodalLM, AutoProcessor, BatchFeature, StoppingCriteria
9
+ from transformers.generation.streamers import TextIteratorStreamer
10
 
11
  MODEL_ID = "google/gemma-4-e4b-it"
12
 
 
 
13
  processor = AutoProcessor.from_pretrained(MODEL_ID, use_fast=False)
14
  model = AutoModelForMultimodalLM.from_pretrained(MODEL_ID, device_map="auto", dtype=torch.bfloat16)
15
+
16
+ IMAGE_FILE_TYPES = (".jpg", ".jpeg", ".png", ".webp")
17
+ AUDIO_FILE_TYPES = (".wav", ".mp3", ".flac", ".ogg")
18
+ VIDEO_FILE_TYPES = (".mp4", ".mov", ".avi", ".webm")
19
+ MAX_INPUT_TOKENS = int(os.getenv("MAX_INPUT_TOKENS", "10_000"))
20
+
21
+ THINKING_START = "<|channel>"
22
+ THINKING_END = "<channel|>"
23
+
24
+ # Special tokens to strip from decoded output (keeping thinking delimiters
25
+ # so that Gradio's reasoning_tags can find them on the frontend).
26
+ _KEEP_TOKENS = {THINKING_START, THINKING_END}
27
+ _STRIP_TOKENS = sorted(
28
+ (t for t in processor.tokenizer.all_special_tokens if t not in _KEEP_TOKENS),
29
+ key=len,
30
+ reverse=True, # longest first to avoid partial matches
31
+ )
32
+
33
+
34
+ def _strip_special_tokens(text: str) -> str:
35
+ for tok in _STRIP_TOKENS:
36
+ text = text.replace(tok, "")
37
+ return text
38
+
39
+
40
+ def _classify_file(path: str) -> str | None:
41
+ """Return media type string for a file path, or None if unsupported."""
42
+ lower = path.lower()
43
+ if lower.endswith(IMAGE_FILE_TYPES):
44
+ return "image"
45
+ if lower.endswith(AUDIO_FILE_TYPES):
46
+ return "audio"
47
+ if lower.endswith(VIDEO_FILE_TYPES):
48
+ return "video"
49
+ return None
50
+
51
+
52
+ def process_new_user_message(message: dict) -> list[dict]:
53
+ """Build content list from the new user message with URL-based media references."""
54
+ content: list[dict] = []
55
+ for path in message.get("files", []):
56
+ kind = _classify_file(path)
57
+ if kind:
58
+ content.append({"type": kind, "url": path})
59
+ content.append({"type": "text", "text": message.get("text", "")})
60
+ return content
61
+
62
+
63
+ def process_history(history: list[dict]) -> list[dict]:
64
+ """Walk Gradio 6 history and build message list with URL-based media references."""
65
+ messages: list[dict] = []
66
+
67
+ for item in history:
68
+ if item["role"] == "assistant":
69
+ if (item.get("metadata") or {}).get("title") == "Reasoning":
70
+ continue
71
+ text_parts = [p["text"] for p in item["content"] if p.get("type") == "text"]
72
+ messages.append(
73
+ {
74
+ "role": "assistant",
75
+ "content": [{"type": "text", "text": " ".join(text_parts)}],
76
+ }
77
+ )
78
+ else:
79
+ user_content: list[dict] = []
80
+ for part in item["content"]:
81
+ if part.get("type") == "text":
82
+ user_content.append({"type": "text", "text": part["text"]})
83
+ elif part.get("type") == "file":
84
+ filepath = part["file"]["path"]
85
+ kind = _classify_file(filepath)
86
+ if kind:
87
+ user_content.append({"type": kind, "url": filepath})
88
+ if user_content:
89
+ messages.append({"role": "user", "content": user_content})
90
+
91
+ return messages
92
+
93
+
94
+ class StopOnSignal(StoppingCriteria):
95
+ def __init__(self) -> None:
96
+ self.stopped = False
97
+
98
+ def __call__(self, input_ids: torch.Tensor, scores: torch.Tensor, **kwargs: object) -> bool: # noqa: ARG002
99
+ return self.stopped
100
+
101
+
102
+ @spaces.GPU(duration=120)
103
+ @torch.inference_mode()
104
+ def _generate_on_gpu(inputs: BatchFeature, max_new_tokens: int, thinking: bool) -> Iterator[str]:
105
+ inputs = inputs.to(device=model.device, dtype=torch.bfloat16)
106
+
107
+ streamer = TextIteratorStreamer(
108
+ processor,
109
+ timeout=30.0,
110
+ skip_prompt=True,
111
+ skip_special_tokens=not thinking,
112
+ )
113
+ stop_criteria = StopOnSignal()
114
+ generate_kwargs = {
115
+ **inputs,
116
+ "streamer": streamer,
117
+ "stopping_criteria": [stop_criteria],
118
+ "max_new_tokens": max_new_tokens,
119
+ "disable_compile": True,
120
+ }
121
+
122
+ exception_holder: list[Exception] = []
123
+
124
+ def _generate() -> None:
125
  try:
126
+ model.generate(**generate_kwargs)
127
+ except Exception as e: # noqa: BLE001
128
+ exception_holder.append(e)
129
+
130
+ thread = Thread(target=_generate)
131
+ thread.start()
 
132
 
133
+ chunks: list[str] = []
134
  try:
135
+ for text in streamer:
136
+ chunks.append(text)
137
+ accumulated = "".join(chunks)
138
+ if thinking:
139
+ yield _strip_special_tokens(accumulated)
140
+ else:
141
+ yield accumulated
142
+ except GeneratorExit:
143
+ stop_criteria.stopped = True
144
+ for _ in streamer:
145
+ pass
146
+ thread.join()
147
+ raise
148
+
149
+ thread.join()
150
+ if exception_holder:
151
+ msg = f"Generation failed: {exception_holder[0]}"
152
+ raise gr.Error(msg)
153
+
154
+
155
+ # FBT003 is suppressed below: gr.validate API takes bool as first positional arg.
156
+ def validate_input(message: dict) -> dict:
157
+ has_text = bool(message.get("text", "").strip())
158
+ has_files = bool(message.get("files"))
159
+ if not (has_text or has_files):
160
+ return gr.validate(False, "Please enter a message or upload a file.") # noqa: FBT003
161
+
162
+ files = message.get("files", [])
163
+ kinds = [_classify_file(f) for f in files]
164
+ kinds = [k for k in kinds if k is not None]
165
+ unique_kinds = set(kinds)
166
+
167
+ if len(unique_kinds) > 1:
168
+ return gr.validate(False, "Please upload only one type of media (images, audio, or video) at a time.") # noqa: FBT003
169
+ if kinds.count("audio") > 1:
170
+ return gr.validate(False, "Only one audio file can be uploaded at a time.") # noqa: FBT003
171
+ if kinds.count("video") > 1:
172
+ return gr.validate(False, "Only one video file can be uploaded at a time.") # noqa: FBT003
173
+
174
+ return gr.validate(True, "") # noqa: FBT003
175
+
176
+
177
+ def _has_media_type(messages: list[dict], media_type: str) -> bool:
178
+ """Check if any message contains a content entry of the given media type."""
179
+ return any(c.get("type") == media_type for m in messages for c in m["content"])
180
+
181
+
182
+ def generate(
183
+ message: dict,
184
+ history: list[dict],
185
+ thinking: bool = False,
186
+ max_new_tokens: int = 1024,
187
+ max_soft_tokens: int = 280,
188
+ system_prompt: str = "",
189
+ ) -> Iterator[str]:
190
+ messages: list[dict] = []
191
+ if system_prompt:
192
+ messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
193
+
194
+ messages.extend(process_history(history))
195
+ messages.append({"role": "user", "content": process_new_user_message(message)})
196
+
197
+ template_kwargs: dict = {
198
+ "tokenize": True,
199
+ "return_dict": True,
200
+ "return_tensors": "pt",
201
+ "add_generation_prompt": True,
202
+ "load_audio_from_video": _has_media_type(messages, "video"),
203
+ "processor_kwargs": {"images_kwargs": {"max_soft_tokens": max_soft_tokens}},
204
+ }
205
+ if thinking:
206
+ template_kwargs["enable_thinking"] = True
207
+
208
+ inputs = processor.apply_chat_template(messages, **template_kwargs)
209
+
210
+ n_tokens = inputs["input_ids"].shape[1]
211
+ if n_tokens > MAX_INPUT_TOKENS:
212
+ msg = f"Input too long ({n_tokens} tokens). Maximum is {MAX_INPUT_TOKENS} tokens."
213
+ raise gr.Error(msg)
214
+
215
+ yield from _generate_on_gpu(inputs=inputs, max_new_tokens=max_new_tokens, thinking=thinking)
216
+
217
+
218
+ examples = [
219
+ # --- Text-only examples ---
220
+ [
221
+ {
222
+ "text": "What is the capital of France?",
223
+ "files": [],
224
+ }
225
+ ],
226
+ [
227
+ {
228
+ "text": "What is the water formula?",
229
+ "files": [],
230
+ }
231
+ ],
232
+ [
233
+ {
234
+ "text": "Explain quantum entanglement in simple terms.",
235
+ "files": [],
236
+ }
237
+ ],
238
+ [
239
+ {
240
+ "text": "I want to do a car wash that is 50 meters away, should I walk or drive?",
241
+ "files": [],
242
+ }
243
+ ],
244
+ [
245
+ {
246
+ "text": "Write a poem about beer with 4 stanzas. Format the title as an H2 markdown heading and bold the first line of each stanza.",
247
+ "files": [],
248
+ }
249
+ ],
250
+ # --- Single-image examples ---
251
+ [
252
+ {
253
+ "text": "Describe this image.",
254
+ "files": ["https://news.bbc.co.uk/media/images/38107000/jpg/_38107299_ronaldogoal_ap_300.jpg"],
255
+ }
256
+ ],
257
+ # --- Multi-image examples ---
258
+ [
259
+ {
260
+ "text": "What are the key similarities between these three images?",
261
+ "files": [
262
+ "https://news.bbc.co.uk/media/images/38107000/jpg/_38107299_ronaldogoal_ap_300.jpg",
263
+ "https://ogimg.infoglobo.com.br/in/12547538-502-0e0/FT1086A/94-8705-14.jpg",
264
+ "https://amazonasatual.com.br/wp-content/uploads/2021/01/Pele.jpg",
265
+ ],
266
+ }
267
+ ],
268
+ # --- Audio examples ---
269
+ [
270
+ {
271
+ "text": "Transcribe the audio.",
272
+ "files": [
273
+ "https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/bcn_weather.mp3"
274
+ ],
275
+ }
276
+ ],
277
+ [
278
+ {
279
+ "text": "Translate to Dutch.",
280
+ "files": [
281
+ "https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/bcn_weather.mp3"
282
+ ],
283
+ }
284
+ ],
285
+ # --- Video examples ---
286
+ [
287
+ {
288
+ "text": "What is happening in this video?",
289
+ "files": ["https://huggingface.co/datasets/merve/vlm_test_images/resolve/main/concert.mp4"],
290
+ }
291
+ ],
292
+ ]
293
 
294
+ demo = gr.ChatInterface(
295
+ fn=generate,
296
+ validator=validate_input,
297
+ chatbot=gr.Chatbot(
298
+ scale=1,
299
+ latex_delimiters=[
300
+ {"left": "$$", "right": "$$", "display": True},
301
+ {"left": "$", "right": "$", "display": False},
302
+ {"left": "\\(", "right": "\\)", "display": False},
303
+ {"left": "\\[", "right": "\\]", "display": True},
304
+ ],
305
+ reasoning_tags=[(THINKING_START, THINKING_END)],
306
+ ),
307
+ textbox=gr.MultimodalTextbox(
308
+ sources=["upload", "microphone"],
309
+ file_types=[*IMAGE_FILE_TYPES, *AUDIO_FILE_TYPES, *VIDEO_FILE_TYPES],
310
+ file_count="multiple",
311
+ autofocus=True,
312
+ stop_btn=True,
313
+ ),
314
+ multimodal=True,
315
+ additional_inputs=[
316
+ gr.Checkbox(label="Thinking", value=False),
317
+ gr.Slider(label="Max New Tokens", minimum=100, maximum=4000, step=10, value=2000),
318
+ gr.Dropdown(
319
+ label="Image Token Budget",
320
+ info="Higher values preserve more visual detail (useful for OCR/documents). Lower values are faster.",
321
+ choices=[70, 140, 280, 560, 1120],
322
+ value=280,
323
+ ),
324
+ gr.Textbox(label="System Prompt", value=""),
325
+ ],
326
+ additional_inputs_accordion=gr.Accordion("Settings", open=True),
327
+ title="Gemma 4 E4B It",
328
+ examples=examples,
329
+ run_examples_on_click=False,
330
+ cache_examples=False,
331
+ delete_cache=(1800, 1800),
332
+ )
333
 
334
  if __name__ == "__main__":
335
+ demo.launch(css_paths="style.css", max_file_size="20MB")