Serg4451D commited on
Commit
ce3af93
·
verified ·
1 Parent(s): f16156d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -77
app.py CHANGED
@@ -1,97 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
- from typing import Generator, List, Tuple
 
3
 
4
  import gradio as gr
5
  from gradio_client import Client, handle_file
6
  from openai import OpenAI
7
 
8
- # --- Конфигурация (в HF Spaces добавь NV_API_KEY в Secrets) ---
9
- NV_API_KEY = os.environ.get("NV_API_KEY")
 
 
 
 
 
 
 
 
 
10
  if not NV_API_KEY:
11
- raise RuntimeError("Добавьте NV_API_KEY в Secrets Hugging Face Space")
 
 
12
 
13
- # Florence-2 (публичный wrapper)
14
- florence = Client("gokaygokay/Florence-2")
15
 
16
- # OpenAI-compatible client (NVIDIA integrate)
17
- llm = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NV_API_KEY)
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- def get_caption(image_path: str) -> str:
21
- """Запрос 'More Detailed Caption' к Florence-2. image_path может быть URL или локальный путь."""
 
 
 
22
  try:
23
- # handle_file поддерживает URL и локальные файлы
24
- result = florence.predict(
25
- image=handle_file(image_path),
 
 
26
  task_prompt="More Detailed Caption",
27
  text_input=None,
28
  model_id="microsoft/Florence-2-large",
29
- api_name="/process_image",
30
  )
31
- # result может быть строкой или структурой — нормализуем
32
- return result if isinstance(result, str) else str(result)
33
  except Exception as e:
 
 
 
34
  return f"[Ошибка при генерац��и подписи: {e}]"
35
 
36
-
37
- def _extract_text_from_chunk(chunk) -> str:
38
- """Универсальная попытка извлечь текстовый фрагмент из stream-chunk."""
 
 
39
  try:
40
- # объект-атрибутный стиль
41
  if hasattr(chunk, "choices"):
42
- choice = chunk.choices[0]
43
- delta = getattr(choice, "delta", None)
44
- if delta is not None:
45
- txt = getattr(delta, "content", None) or getattr(delta, "reasoning_content", None)
46
- return txt or ""
47
- # dict-стиль
 
 
 
 
 
 
 
 
48
  if isinstance(chunk, dict):
49
- choices = chunk.get("choices", [])
50
  if choices:
51
- delta = choices[0].get("delta", {})
52
- return delta.get("content") or delta.get("reasoning_content") or ""
 
53
  except Exception:
54
- return ""
55
  return ""
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- def chat_stream(image_path: str, user_message: str, history: List[Tuple[str, str]]):
59
  """
60
- Generator для Gradio: сначала возвращает caption, затем по мере прихода токенов
61
- обновляет последний ответ ассистента.
62
- Возвращаемые значения кортежи (history, caption) соответствующие outputs.
63
  """
64
  history = history or []
 
 
 
 
 
65
 
66
- if not image_path:
67
- history.append([user_message, "Пожалуйста, загрузите изображение."])
68
- yield history, ""
 
69
  return
70
 
71
- # Получаем подробную подпись
72
- caption = get_caption(image_path)
 
 
 
 
 
 
73
 
74
- # Сборка системного промпта
75
  system_prompt = (
76
- "You are 'multimodal gpt-oss 120b'. Use the provided 'More Detailed Caption' as authoritative visual context.\n\n"
 
 
77
  "Image Caption START >>>\n"
78
  f"{caption}\n"
79
  "<<< Image Caption END.\n"
80
- "When answering, mention visible details and be explicit when uncertain."
 
81
  )
82
 
83
- # Добавляем сообщение пользователя
84
- history.append([user_message, ""])
85
- # Первый yield чтобы UI сразу показал пользовательское сообщение и подпись
86
  yield history, caption
87
 
88
- assistant_text = ""
89
  try:
 
90
  stream = llm.chat.completions.create(
91
  model="openai/gpt-oss-120b",
92
  messages=[
93
  {"role": "system", "content": system_prompt},
94
- {"role": "user", "content": user_message},
95
  ],
96
  temperature=0.8,
97
  top_p=1.0,
@@ -100,62 +216,89 @@ def chat_stream(image_path: str, user_message: str, history: List[Tuple[str, str
100
  )
101
 
102
  for chunk in stream:
103
- piece = _extract_text_from_chunk(chunk)
104
  if not piece:
105
  continue
106
- assistant_text += piece
107
- history[-1][1] = assistant_text
 
108
  yield history, caption
109
 
110
  except Exception as e:
111
- # В случае ошибки покажем её в чате
112
- history[-1][1] = f"[Ошибка стриминга LLM: {e}]"
113
- yield history, caption
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- # Финальный yield (гарантируем состояние завершения)
116
  yield history, caption
117
 
118
-
119
- # --- UI (для HF Spaces) ---
120
  EXAMPLE_IMAGES = [
121
- # список простых строк (URL или локальные пути). НИКАКИХ вложенных списков!
122
  "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
123
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png",
124
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cheetah.jpg",
125
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/flowers.png",
126
  ]
127
 
 
128
  css = """
129
- #title {text-align:center; margin-bottom: -18px;}
130
  .gradio-container { max-width: 1100px; margin: auto; }
 
131
  """
132
 
133
- with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
134
  gr.Markdown("<h2 id='title'>🖼️ multimodal gpt-oss 120b — визуальный чат</h2>")
135
  with gr.Row():
136
  with gr.Column(scale=4):
137
- image_input = gr.Image(label="Загрузите картинку или выберите из галереи", type="filepath", tool="editor")
138
- raw_caption = gr.Textbox(label="More Detailed Caption (Florence-2)", interactive=False)
139
- user_input = gr.Textbox(label="Вопрос по изображению", placeholder="Например: 'Что происходит на фото?'")
140
  send_btn = gr.Button("Отправить")
141
  clear_btn = gr.Button("Очистить чат")
142
- gr.Markdown("**Галерея примеров (клик — подставить в загрузчик)**")
143
- gallery = gr.Gallery(value=EXAMPLE_IMAGES, columns=4, label="Примеры", show_label=False).style(grid=[4], height="auto")
144
 
145
  with gr.Column(scale=6):
146
- chatbot = gr.Chatbot(label="Чат с моделью", height=600)
147
-
148
- # Клик по картинке в галерее -> вставляем URL/путь в image_input
149
- def pick_example(img_url: str):
150
- return img_url
151
-
152
- gallery.select(fn=pick_example, inputs=[gallery], outputs=[image_input])
153
-
154
- # Кнопка отправки: привязываем генератор, который возвращает (chat_history, caption)
155
- send_btn.click(fn=chat_stream, inputs=[image_input, user_input, chatbot], outputs=[chatbot, raw_caption])
156
 
157
- clear_btn.click(lambda: [], None, chatbot)
 
158
 
159
- if __name__ == "__main__":
160
- demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
161
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ multimodal gpt-oss 120b — Gradio app for Hugging Face Spaces
4
+
5
+ Функции:
6
+ - Загрузка собственной картинки (type="filepath")
7
+ - Галерея примеров (клик -> подставляет в загрузчик)
8
+ - Автогенерация "More Detailed Caption" через gradio_client Florence-2
9
+ - Streaming ответов от openai/gpt-oss-120b (через NVIDIA integrate / OpenAI-compatible)
10
+ - Кеширование подписи для одной и той же картинки
11
+ """
12
+
13
  import os
14
+ import traceback
15
+ from typing import Any, Dict, List, Tuple, Optional
16
 
17
  import gradio as gr
18
  from gradio_client import Client, handle_file
19
  from openai import OpenAI
20
 
21
+ # (опционально) локальный .env при локальном запуске
22
+ try:
23
+ from dotenv import load_dotenv
24
+ load_dotenv()
25
+ except Exception:
26
+ pass
27
+
28
+ # --------------------- Конфигурация ---------------------
29
+ NV_API_KEY = os.environ.get("NV_API_KEY") # ОБЯЗАТЕЛЬНО прописать в Secrets HF Spaces
30
+ NV_BASE_URL = os.environ.get("NV_BASE_URL", "https://integrate.api.nvidia.com/v1")
31
+
32
  if not NV_API_KEY:
33
+ raise RuntimeError(
34
+ "NV_API_KEY не задан. В Hugging Face Space зайди в Settings → Secrets и добавь NV_API_KEY."
35
+ )
36
 
37
+ # Florence-2 Gradio wrapper (публичный)
38
+ FLORENCE_WRAPPER = "gokaygokay/Florence-2"
39
 
40
+ # --------------------- Клиенты ---------------------
41
+ florence = Client(FLORENCE_WRAPPER)
42
+ llm = OpenAI(base_url=NV_BASE_URL, api_key=NV_API_KEY)
43
 
44
+ # --------------------- Хелперы ---------------------
45
+ def _normalize_florence_result(res: Any) -> str:
46
+ """
47
+ Нормализует результат predict от Florence-2: возвращает текстовую подпись.
48
+ Подстраховываемся на разные форматы (строка, dict, list и т.д.).
49
+ """
50
+ try:
51
+ if res is None:
52
+ return ""
53
+ if isinstance(res, str):
54
+ return res
55
+ # dict-like
56
+ if isinstance(res, dict):
57
+ # часто бывает ключ 'caption' или 'text' или 'generated_text'
58
+ for k in ("caption", "text", "generated_text", "output", "result"):
59
+ if k in res and isinstance(res[k], str):
60
+ return res[k]
61
+ # если есть nested fields, попробуем взять первое строковое значение
62
+ for v in res.values():
63
+ if isinstance(v, str):
64
+ return v
65
+ # fallback: str()
66
+ return str(res)
67
+ # list/tuple: join string elements
68
+ if isinstance(res, (list, tuple)):
69
+ pieces = [str(x) for x in res]
70
+ return "\n".join(pieces)
71
+ # other types: fallback to str
72
+ return str(res)
73
+ except Exception:
74
+ return f"[Ошибка нормализации подписи: {traceback.format_exc()}]"
75
 
76
+ def get_caption_for_image(image_path_or_url: str, safety_note: bool = False) -> str:
77
+ """
78
+ Запрос к Florence-2: task_prompt="More Detailed Caption".
79
+ Принимает локальный путь или URL.
80
+ """
81
  try:
82
+ if not image_path_or_url:
83
+ return ""
84
+ # handle_file поддерживает URL и локальные пути
85
+ res = florence.predict(
86
+ image=handle_file(image_path_or_url),
87
  task_prompt="More Detailed Caption",
88
  text_input=None,
89
  model_id="microsoft/Florence-2-large",
90
+ api_name="/process_image"
91
  )
92
+ caption = _normalize_florence_result(res)
93
+ return caption
94
  except Exception as e:
95
+ # логируем в stdout (HF Spaces покажет лог)
96
+ print("Ошибка Florence-2 predict:", e)
97
+ traceback.print_exc()
98
  return f"[Ошибка при генерац��и подписи: {e}]"
99
 
100
+ def _extract_text_from_stream_chunk(chunk: Any) -> str:
101
+ """
102
+ Универсально извлекает текстовые фрагменты из чанка стриминга LLM.
103
+ Работает с разными формами chunk (объект SDK, dict, ...)
104
+ """
105
  try:
106
+ # объектный стиль: chunk.choices[0].delta.content
107
  if hasattr(chunk, "choices"):
108
+ choices = getattr(chunk, "choices")
109
+ if choices:
110
+ c0 = choices[0]
111
+ delta = getattr(c0, "delta", None)
112
+ if delta is not None:
113
+ # reasoning_content или content
114
+ txt = getattr(delta, "reasoning_content", None) or getattr(delta, "content", None)
115
+ if txt:
116
+ return str(txt)
117
+ # some SDK might put content in c0.get("text") etc.
118
+ text_attr = getattr(c0, "text", None)
119
+ if text_attr:
120
+ return str(text_attr)
121
+ # dict-style
122
  if isinstance(chunk, dict):
123
+ choices = chunk.get("choices") or []
124
  if choices:
125
+ delta = choices[0].get("delta") or {}
126
+ # try common keys
127
+ return str(delta.get("content") or delta.get("reasoning_content") or choices[0].get("text") or "")
128
  except Exception:
129
+ pass
130
  return ""
131
 
132
+ # --------------------- UI-логика ---------------------
133
+ # Кеш подписи (чтобы не вызывать Florence снова для той же картинки)
134
+ # Храним словарь: {"image_path": "...", "caption": "..."}
135
+ # Будем использовать gr.State для хранения этого словаря в сессии
136
+ def generate_and_cache_caption(image, cache: Optional[Dict[str, str]]):
137
+ """
138
+ Вызывается при изменении image_input или при клике по галерее.
139
+ Возвращает (caption_text, new_cache_dict).
140
+ """
141
+ try:
142
+ if not image:
143
+ return "", {"image_path": None, "caption": None}
144
+ # Готовим path/URL
145
+ img_path = image if isinstance(image, str) else getattr(image, "name", None) or image
146
+ # Проверка кеша
147
+ if cache and cache.get("image_path") == img_path and cache.get("caption"):
148
+ return cache.get("caption"), cache
149
+ # Иначе генерируем подпись
150
+ caption = get_caption_for_image(img_path)
151
+ new_cache = {"image_path": img_path, "caption": caption}
152
+ return caption, new_cache
153
+ except Exception as e:
154
+ print("generate_and_cache_caption exception:", e)
155
+ traceback.print_exc()
156
+ return f"[Ошибка генерации подписи: {e}]", {"image_path": None, "caption": None}
157
 
158
+ def chat_stream(image, user_message, history, cache: Dict[str, str]):
159
  """
160
+ Основной generator для кнопки Отправить / submit:
161
+ - Автоматически использует кеш подписи (если есть), иначе генерирует новую
162
+ - Возвращает по мере стриминга (history, caption) соответствие outputs=[chatbot, raw_caption]
163
  """
164
  history = history or []
165
+ # Проверки входа
166
+ if not user_message:
167
+ # ничего не делаем, просто возвращаем текущее состояние
168
+ yield history, (cache.get("caption") if cache else "")
169
+ return
170
 
171
+ if not image:
172
+ # если нет картинки — говорим пользователю
173
+ history.append([user_message, "Пожалуйста, загрузите изображение или выберите из галереи."])
174
+ yield history, (cache.get("caption") if cache else "")
175
  return
176
 
177
+ # получить путь и подпись (используем кеш, если совпадает)
178
+ img_path = image if isinstance(image, str) else getattr(image, "name", None) or image
179
+ if cache and cache.get("image_path") == img_path and cache.get("caption"):
180
+ caption = cache.get("caption")
181
+ else:
182
+ caption = get_caption_for_image(img_path)
183
+ # обновляем кеш локально (не gr.State, а для текущего запроса)
184
+ cache = {"image_path": img_path, "caption": caption}
185
 
186
+ # система-промпт даём контекст и просим указывать степень уверенности
187
  system_prompt = (
188
+ "You are 'multimodal gpt-oss 120b', a helpful multimodal assistant. "
189
+ "Use the provided 'More Detailed Caption' as authoritative visual context. "
190
+ "If something is not visible or certain, say so explicitly.\n\n"
191
  "Image Caption START >>>\n"
192
  f"{caption}\n"
193
  "<<< Image Caption END.\n"
194
+ "Answer the user's question based on the caption and general knowledge. "
195
+ "Be concise unless asked for details."
196
  )
197
 
198
+ # добавляем пользовательский запрос в историю (пустой ответ пока)
199
+ history.append([user_message, ""]) # assistant текст будет заполняться по мере стрима
200
+ # первый yield чтобы UI сразу отобразил user's message и подпись
201
  yield history, caption
202
 
203
+ assistant_accum = ""
204
  try:
205
+ # Запускаем стриминг вызов
206
  stream = llm.chat.completions.create(
207
  model="openai/gpt-oss-120b",
208
  messages=[
209
  {"role": "system", "content": system_prompt},
210
+ {"role": "user", "content": user_message}
211
  ],
212
  temperature=0.8,
213
  top_p=1.0,
 
216
  )
217
 
218
  for chunk in stream:
219
+ piece = _extract_text_from_stream_chunk(chunk)
220
  if not piece:
221
  continue
222
+ assistant_accum += piece
223
+ # обновляем последний элемент истории (assistant part)
224
+ history[-1][1] = assistant_accum
225
  yield history, caption
226
 
227
  except Exception as e:
228
+ # Ошибка стриминга: попробуем получить финальный ответ без стрима, либо показать ошибку
229
+ print("Streaming error:", e)
230
+ traceback.print_exc()
231
+ # Пытаемся сделать не-стриминг вызов (fallback)
232
+ try:
233
+ resp = llm.chat.completions.create(
234
+ model="openai/gpt-oss-120b",
235
+ messages=[
236
+ {"role": "system", "content": system_prompt},
237
+ {"role": "user", "content": user_message}
238
+ ],
239
+ temperature=0.8,
240
+ top_p=1.0,
241
+ max_tokens=1024,
242
+ stream=False,
243
+ )
244
+ # нормализуем возможный формат ответа
245
+ final_text = ""
246
+ # SDK может вернуть object-like resp.choices[0].message.content
247
+ if hasattr(resp, "choices"):
248
+ try:
249
+ final_text = getattr(resp.choices[0].message, "content", "") or getattr(resp.choices[0], "text", "") or ""
250
+ except Exception:
251
+ final_text = str(resp)
252
+ elif isinstance(resp, dict):
253
+ choices = resp.get("choices", [])
254
+ if choices:
255
+ m = choices[0].get("message") or choices[0]
256
+ final_text = m.get("content") or m.get("text") or str(m)
257
+ else:
258
+ final_text = str(resp)
259
+ else:
260
+ final_text = str(resp)
261
+ history[-1][1] = final_text
262
+ yield history, caption
263
+ except Exception as e2:
264
+ history[-1][1] = f"[Ошибка LLM: {e2}]"
265
+ yield history, caption
266
 
267
+ # финальный yield (гарантируем окончательное состояние)
268
  yield history, caption
269
 
270
+ # --------------------- Примеры для галереи (список строк) ---------------------
 
271
  EXAMPLE_IMAGES = [
 
272
  "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
273
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png",
274
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cheetah.jpg",
275
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/flowers.png",
276
  ]
277
 
278
+ # --------------------- UI ---------------------
279
  css = """
 
280
  .gradio-container { max-width: 1100px; margin: auto; }
281
+ #title { text-align: center; }
282
  """
283
 
284
+ with gr.Blocks(css=css, analytics_enabled=False) as demo:
285
  gr.Markdown("<h2 id='title'>🖼️ multimodal gpt-oss 120b — визуальный чат</h2>")
286
  with gr.Row():
287
  with gr.Column(scale=4):
288
+ image_input = gr.Image(label="Загрузите картинку (файл / drag-n-drop / камера)", type="filepath")
289
+ raw_caption = gr.Textbox(label="More Detailed Caption (Florence-2)", interactive=False, lines=6)
290
+ user_input = gr.Textbox(label="Вопрос по изображению", placeholder="Например: Что происходит на фото?")
291
  send_btn = gr.Button("Отправить")
292
  clear_btn = gr.Button("Очистить чат")
293
+ gr.Markdown("**Галерея примеров (клик — подставить в загрузчик и получить подпись)**")
294
+ gallery = gr.Gallery(value=EXAMPLE_IMAGES, label="Примеры", columns=4, show_label=False).style(grid=[4])
295
 
296
  with gr.Column(scale=6):
297
+ chatbot = gr.Chatbot(label="Чат с моделью", height=640)
 
 
 
 
 
 
 
 
 
298
 
299
+ # gr.State для кеша подписи
300
+ caption_cache = gr.State(value={"image_path": None, "caption": None})
301
 
302
+ # обработчик клика по галерее: сразу подставляет картинку, генерирует подпись и обновляет кеш
303
+ def on_gallery_select(elem, cach
304