Serg4451D commited on
Commit
92e249a
·
verified ·
1 Parent(s): 5d2ac6e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -59
app.py CHANGED
@@ -1,11 +1,10 @@
1
  #!/usr/bin/env python3
2
  """
3
- multimodal gpt-oss 120b — Gradio app с Florence-2 в браузере (WebGPU)
4
 
5
- Что изменилось:
6
- - Подпись к изображению генерим на стороне пользователя (WebGPU) через Transformers.js.
7
- - Сервер больше не грузит Florence/torch.
8
- - LLM остаётся через NVIDIA Integrate (OpenAI-compatible), как и было.
9
  """
10
 
11
  import os
@@ -30,7 +29,7 @@ if not NV_API_KEY:
30
  "NV_API_KEY не задан. В Hugging Face Space зайди в Settings → Secrets и добавь NV_API_KEY."
31
  )
32
 
33
- # OpenAI клиент для LLM
34
  llm = OpenAI(base_url=NV_BASE_URL, api_key=NV_API_KEY)
35
 
36
 
@@ -64,7 +63,7 @@ def _extract_text_from_stream_chunk(chunk: Any) -> str:
64
  def chat_stream(image, user_message: str, history: Optional[List[List[str]]], caption_text: str):
65
  """
66
  Основной generator для стриминга ответов LLM.
67
- Теперь принимает caption_text прямо из браузера (WebGPU).
68
  """
69
  history = history or []
70
 
@@ -79,7 +78,6 @@ def chat_stream(image, user_message: str, history: Optional[List[List[str]]], ca
79
 
80
  caption = caption_text or ""
81
 
82
- # Системный промпт с подписью
83
  system_prompt = (
84
  "You are 'multimodal gpt-oss 120b', a helpful multimodal assistant. "
85
  "Use the provided 'More Detailed Caption' as authoritative visual context. "
@@ -91,14 +89,11 @@ def chat_stream(image, user_message: str, history: Optional[List[List[str]]], ca
91
  "Be concise unless asked for details."
92
  )
93
 
94
- # Добавляем сообщение пользователя
95
  history.append([user_message, ""])
96
- # Показать подпись справа от чата (как и раньше)
97
  yield history, caption
98
 
99
  assistant_accum = ""
100
  try:
101
- # Стриминг от LLM
102
  stream = llm.chat.completions.create(
103
  model="openai/gpt-oss-120b",
104
  messages=[
@@ -122,7 +117,6 @@ def chat_stream(image, user_message: str, history: Optional[List[List[str]]], ca
122
  except Exception as e:
123
  print(f"Streaming error: {e}")
124
  traceback.print_exc()
125
- # Fallback на не-стриминг запрос
126
  try:
127
  resp = llm.chat.completions.create(
128
  model="openai/gpt-oss-120b",
@@ -173,27 +167,60 @@ css = """
173
  #title { text-align: center; }
174
  """
175
 
176
- # JS-функция: делает caption в браузере через WebGPU (Transformers.js)
177
  WEBGPU_CAPTION_JS = r"""
178
  async (image, use_client) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  try {
180
  if (!use_client) return null;
181
 
182
- if (!('gpu' in navigator)) {
183
- return "[WebGPU недоступен в браузере. Chrome/Edge 113+ (на Linux — chrome://flags/#enable-unsafe-webgpu), Safari TP.]";
 
 
184
  }
185
 
186
- // Извлекаем источник изображения из значения Gradio Image
187
  const toHTMLImage = async (imgVal) => {
188
  if (!imgVal) throw new Error("Нет изображения");
189
  let src = null;
190
- if (typeof imgVal === 'string') {
191
- src = imgVal;
192
- } else if (imgVal?.image) {
193
- src = imgVal.image;
194
- } else if (imgVal?.data) {
195
- src = imgVal.data;
196
- }
197
  if (!src) throw new Error("Не удалось прочитать изображение");
198
  const im = new Image();
199
  im.crossOrigin = 'anonymous';
@@ -202,49 +229,81 @@ async (image, use_client) => {
202
  return im;
203
  };
204
 
205
- // Подтягиваем Transformers.js
206
- const { pipeline, env } = await import("https://cdn.jsdelivr.net/npm/@xenova/[email protected]");
 
 
207
 
208
- // Предпочесть WebGPU
209
  env.allowRemoteModels = true;
210
- env.useBrowserCache = true; // кэш в IndexedDB
211
- env.backends.onnx.backend = 'webgpu';
212
-
213
- // Инициализация один раз
214
- if (!window.__webgpu_captioner) {
215
- const candidates = [
216
- 'Xenova/Florence-2-large-ft',
217
- 'Xenova/Florence-2-base-ft'
218
- ];
 
 
 
 
 
 
 
 
 
219
  let lastErr = null;
220
  for (const model of candidates) {
221
  try {
222
- window.__webgpu_captioner = await pipeline(
223
- 'image-to-text',
224
- model,
225
- { device: 'webgpu', dtype: 'fp16', quantized: true }
226
- );
 
 
 
 
 
 
 
 
 
227
  break;
228
  } catch (e) {
229
  lastErr = e;
230
- console.warn('Failed to load', model, e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
  }
233
- if (!window.__webgpu_captioner) throw lastErr || new Error("Не удалось инициализировать captioner");
234
  }
235
 
236
  const imgEl = await toHTMLImage(image);
237
-
238
- // Для Florence-2 более детальная подпись через специальный токен задачи
239
- const out = await window.__webgpu_captioner(imgEl, { text: '<MORE_DETAILED_CAPTION>' });
240
-
241
  const text = Array.isArray(out)
242
  ? (out[0]?.generated_text ?? out[0]?.text ?? JSON.stringify(out[0]))
243
  : (out?.generated_text ?? out?.text ?? String(out));
244
-
245
  return text;
246
  } catch (e) {
247
- return `[WebGPU caption error: ${'message' in e ? e.message : e}]`;
248
  }
249
  }
250
  """
@@ -255,12 +314,12 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
255
  with gr.Row():
256
  with gr.Column(scale=4):
257
  image_input = gr.Image(label="Загрузите картинку", type="filepath")
258
- use_webgpu = gr.Checkbox(value=True, label="Генерировать подпись к изображению в браузере (WebGPU)")
259
  raw_caption = gr.Textbox(
260
- label="More Detailed Caption (WebGPU)",
261
- interactive=True,
262
  lines=6,
263
- placeholder="Подпись появится тут (если включён WebGPU-капшенер)"
264
  )
265
  user_input = gr.Textbox(
266
  label="Вопрос по изображению",
@@ -270,7 +329,7 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
270
  send_btn = gr.Button("Отправить", variant="primary")
271
  clear_btn = gr.Button("Очистить чат")
272
 
273
- gr.Markdown("**Галерея примеров (клик — подставить в загрузчик, подпись посчитается в браузере)**")
274
  gallery = gr.Gallery(
275
  value=EXAMPLE_IMAGES,
276
  label="Примеры",
@@ -284,7 +343,7 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
284
  with gr.Column(scale=6):
285
  chatbot = gr.Chatbot(label="Чат с моделью", height=640)
286
 
287
- # Клик по галерее: просто подставить изображение и очистить подпись (капшенер сработает на change)
288
  def on_gallery_select(evt: gr.SelectData):
289
  img = EXAMPLE_IMAGES[evt.index]
290
  return img, ""
@@ -295,7 +354,7 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
295
  outputs=[image_input, raw_caption]
296
  )
297
 
298
- # Изменение картинки: считаем подпись на клиенте (WebGPU)
299
  image_input.change(
300
  None,
301
  inputs=[image_input, use_webgpu],
@@ -303,13 +362,20 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
303
  js=WEBGPU_CAPTION_JS
304
  )
305
 
306
- # Отправка сообщения: берём caption прямо из текстбокса (не генерим на сервере)
 
 
 
 
 
 
 
 
307
  send_btn.click(
308
  chat_stream,
309
  inputs=[image_input, user_input, chatbot, raw_caption],
310
  outputs=[chatbot, raw_caption]
311
  )
312
-
313
  user_input.submit(
314
  chat_stream,
315
  inputs=[image_input, user_input, chatbot, raw_caption],
@@ -318,12 +384,12 @@ with gr.Blocks(css=css, analytics_enabled=False) as demo:
318
 
319
  # Очистка чата + подписи
320
  def clear_all():
321
- return [], ""
322
 
323
  clear_btn.click(
324
  clear_all,
325
  inputs=None,
326
- outputs=[chatbot, raw_caption]
327
  )
328
 
329
  # Запуск
 
1
  #!/usr/bin/env python3
2
  """
3
+ multimodal gpt-oss 120b — Gradio app: Florence в браузере (WebGPU), LLM через NVIDIA Integrate
4
 
5
+ - Подпись к изображению генерится на клиенте (Transformers.js + WebGPU / wasm фоллбэк).
6
+ - Сервер НЕ грузит torch/Florence.
7
+ - LLM-стриминг как раньше (openai/gpt-oss-120b).
 
8
  """
9
 
10
  import os
 
29
  "NV_API_KEY не задан. В Hugging Face Space зайди в Settings → Secrets и добавь NV_API_KEY."
30
  )
31
 
32
+ # OpenAI совместимый клиент для LLM
33
  llm = OpenAI(base_url=NV_BASE_URL, api_key=NV_API_KEY)
34
 
35
 
 
63
  def chat_stream(image, user_message: str, history: Optional[List[List[str]]], caption_text: str):
64
  """
65
  Основной generator для стриминга ответов LLM.
66
+ Теперь caption_text приходит напрямую из браузера (WebGPU/wasm).
67
  """
68
  history = history or []
69
 
 
78
 
79
  caption = caption_text or ""
80
 
 
81
  system_prompt = (
82
  "You are 'multimodal gpt-oss 120b', a helpful multimodal assistant. "
83
  "Use the provided 'More Detailed Caption' as authoritative visual context. "
 
89
  "Be concise unless asked for details."
90
  )
91
 
 
92
  history.append([user_message, ""])
 
93
  yield history, caption
94
 
95
  assistant_accum = ""
96
  try:
 
97
  stream = llm.chat.completions.create(
98
  model="openai/gpt-oss-120b",
99
  messages=[
 
117
  except Exception as e:
118
  print(f"Streaming error: {e}")
119
  traceback.print_exc()
 
120
  try:
121
  resp = llm.chat.completions.create(
122
  model="openai/gpt-oss-120b",
 
167
  #title { text-align: center; }
168
  """
169
 
170
+ # JS: надёжная загрузка Transformers.js (ESM UMD → локальный), WebGPU/wasm, Florence-2 large→base
171
  WEBGPU_CAPTION_JS = r"""
172
  async (image, use_client) => {
173
+ const loadWithScript = (url) => new Promise((res, rej) => {
174
+ const s = document.createElement('script');
175
+ s.src = url; s.async = true;
176
+ s.onload = () => res();
177
+ s.onerror = (e) => rej(e);
178
+ document.head.appendChild(s);
179
+ });
180
+
181
+ const loadTransformers = async () => {
182
+ // 1) ESM: несколько CDN
183
+ const esms = [
184
+ 'https://cdn.jsdelivr.net/npm/@xenova/[email protected]',
185
+ 'https://cdn.jsdelivr.net/npm/@xenova/[email protected]?module',
186
+ 'https://unpkg.com/@xenova/[email protected]?module',
187
+ 'https://esm.run/@xenova/[email protected]',
188
+ ];
189
+ for (const url of esms) {
190
+ try { return await import(url); }
191
+ catch (e) { console.warn('ESM import failed:', url, e); }
192
+ }
193
+ // 2) UMD: глобальная window.transformers
194
+ const umds = [
195
+ 'https://cdn.jsdelivr.net/npm/@xenova/[email protected]/dist/transformers.min.js',
196
+ 'https://unpkg.com/@xenova/[email protected]/dist/transformers.min.js',
197
+ // локальный фоллбэк (если положите файл в репозиторий)
198
+ '/file=assets/transformers-3.2.2.min.js',
199
+ ];
200
+ for (const url of umds) {
201
+ try {
202
+ await loadWithScript(url);
203
+ if (window.transformers) return window.transformers;
204
+ } catch (e) { console.warn('UMD load failed:', url, e); }
205
+ }
206
+ throw new Error('Transformers.js: все CDN/UMD источники недоступны');
207
+ };
208
+
209
  try {
210
  if (!use_client) return null;
211
 
212
+ // WebGPU наличие
213
+ const hasWebGPU = 'gpu' in navigator;
214
+ if (!hasWebGPU) {
215
+ console.warn('WebGPU недоступен, будет попытка wasm-фоллбэка.');
216
  }
217
 
 
218
  const toHTMLImage = async (imgVal) => {
219
  if (!imgVal) throw new Error("Нет изображения");
220
  let src = null;
221
+ if (typeof imgVal === 'string') src = imgVal;
222
+ else if (imgVal?.image) src = imgVal.image;
223
+ else if (imgVal?.data) src = imgVal.data;
 
 
 
 
224
  if (!src) throw new Error("Не удалось прочитать изображение");
225
  const im = new Image();
226
  im.crossOrigin = 'anonymous';
 
229
  return im;
230
  };
231
 
232
+ const tjs = await loadTransformers();
233
+ const pipeline = tjs.pipeline ?? tjs?.default?.pipeline;
234
+ const env = tjs.env ?? tjs?.default?.env;
235
+ if (!pipeline || !env) throw new Error("Transformers.js загрузился без pipeline/env");
236
 
237
+ // Настройка бэкенда и кэша
238
  env.allowRemoteModels = true;
239
+ env.useBrowserCache = true;
240
+ env.backends ??= {};
241
+ env.backends.onnx ??= {};
242
+ // Путь к wasm-артефактам (на всякий)
243
+ env.backends.onnx.wasm ??= {};
244
+ env.backends.onnx.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/@xenova/[email protected]/dist/wasm/';
245
+ env.backends.onnx.wasm.numThreads = 1;
246
+
247
+ // Пробуем WebGPU, fallback → wasm
248
+ let backend = 'webgpu';
249
+ if (!hasWebGPU) backend = 'wasm';
250
+
251
+ // Инициализация captioner (один раз)
252
+ if (!window.__web_captioner || window.__web_captioner_backend !== backend) {
253
+ const candidates = backend === 'webgpu'
254
+ ? ['Xenova/Florence-2-large-ft', 'Xenova/Florence-2-base-ft']
255
+ : ['Xenova/Florence-2-base-ft'];
256
+
257
  let lastErr = null;
258
  for (const model of candidates) {
259
  try {
260
+ if (backend === 'webgpu') {
261
+ env.backends.onnx.backend = 'webgpu';
262
+ window.__web_captioner = await pipeline(
263
+ 'image-to-text', model,
264
+ { device: 'webgpu', dtype: 'fp16', quantized: true }
265
+ );
266
+ } else {
267
+ env.backends.onnx.backend = 'wasm';
268
+ window.__web_captioner = await pipeline(
269
+ 'image-to-text', model,
270
+ { device: 'wasm', quantized: true }
271
+ );
272
+ }
273
+ window.__web_captioner_backend = backend;
274
  break;
275
  } catch (e) {
276
  lastErr = e;
277
+ console.warn(`Init failed (${backend}) for ${model}`, e);
278
+ }
279
+ }
280
+ if (!window.__web_captioner) {
281
+ // Последняя попытка: если WebGPU падал — откат на wasm
282
+ if (backend === 'webgpu') {
283
+ try {
284
+ env.backends.onnx.backend = 'wasm';
285
+ window.__web_captioner = await pipeline(
286
+ 'image-to-text', 'Xenova/Florence-2-base-ft',
287
+ { device: 'wasm', quantized: true }
288
+ );
289
+ window.__web_captioner_backend = 'wasm';
290
+ } catch (e2) {
291
+ throw lastErr || e2;
292
+ }
293
+ } else {
294
+ throw lastErr || new Error('Не удалось инициализировать captioner');
295
  }
296
  }
 
297
  }
298
 
299
  const imgEl = await toHTMLImage(image);
300
+ const out = await window.__web_captioner(imgEl, { text: '<MORE_DETAILED_CAPTION>' });
 
 
 
301
  const text = Array.isArray(out)
302
  ? (out[0]?.generated_text ?? out[0]?.text ?? JSON.stringify(out[0]))
303
  : (out?.generated_text ?? out?.text ?? String(out));
 
304
  return text;
305
  } catch (e) {
306
+ return `[WebGPU caption error: ${e?.message || e}]`;
307
  }
308
  }
309
  """
 
314
  with gr.Row():
315
  with gr.Column(scale=4):
316
  image_input = gr.Image(label="Загрузите картинку", type="filepath")
317
+ use_webgpu = gr.Checkbox(value=True, label="Генерировать подпись к изображению в браузере (WebGPU/wasm)")
318
  raw_caption = gr.Textbox(
319
+ label="More Detailed Caption (клиентский Florence-2)",
320
+ interactive=True,
321
  lines=6,
322
+ placeholder="Подпись появится тут (WebGPU/wasm)"
323
  )
324
  user_input = gr.Textbox(
325
  label="Вопрос по изображению",
 
329
  send_btn = gr.Button("Отправить", variant="primary")
330
  clear_btn = gr.Button("Очистить чат")
331
 
332
+ gr.Markdown("Галерея примеров (клик — подставить в загрузчик, подпись посчитается на клиенте)")
333
  gallery = gr.Gallery(
334
  value=EXAMPLE_IMAGES,
335
  label="Примеры",
 
343
  with gr.Column(scale=6):
344
  chatbot = gr.Chatbot(label="Чат с моделью", height=640)
345
 
346
+ # Клик по галерее: подставить изображение и очистить подпись (капшенер сработает на change)
347
  def on_gallery_select(evt: gr.SelectData):
348
  img = EXAMPLE_IMAGES[evt.index]
349
  return img, ""
 
354
  outputs=[image_input, raw_caption]
355
  )
356
 
357
+ # Изменение картинки: считаем подпись на клиенте (WebGPU/wasm)
358
  image_input.change(
359
  None,
360
  inputs=[image_input, use_webgpu],
 
362
  js=WEBGPU_CAPTION_JS
363
  )
364
 
365
+ # Переключение флажка: если включили пересчитать подпись; если выключили очистить
366
+ use_webgpu.change(
367
+ None,
368
+ inputs=[image_input, use_webgpu],
369
+ outputs=[raw_caption],
370
+ js=WEBGPU_CAPTION_JS
371
+ )
372
+
373
+ # Отправка сообщения: берём caption прямо из текстбокса (клиентский Florence)
374
  send_btn.click(
375
  chat_stream,
376
  inputs=[image_input, user_input, chatbot, raw_caption],
377
  outputs=[chatbot, raw_caption]
378
  )
 
379
  user_input.submit(
380
  chat_stream,
381
  inputs=[image_input, user_input, chatbot, raw_caption],
 
384
 
385
  # Очистка чата + подписи
386
  def clear_all():
387
+ return [], "", ""
388
 
389
  clear_btn.click(
390
  clear_all,
391
  inputs=None,
392
+ outputs=[chatbot, raw_caption, user_input]
393
  )
394
 
395
  # Запуск