IFMedTechdemo commited on
Commit
a35d993
·
verified ·
1 Parent(s): 79c09f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -214
app.py CHANGED
@@ -2,21 +2,20 @@
2
 
3
 
4
  import os
5
- import time
6
- from threading import Thread
7
  from typing import Iterable, Dict, Any, Optional, List
 
 
8
 
9
  import gradio as gr
10
  import spaces
11
  import torch
12
  from PIL import Image
13
- import pandas as pd # Excel read + debug
14
 
15
  from transformers import (
16
  Qwen3VLForConditionalGeneration,
17
  AutoModelForCausalLM,
18
  AutoProcessor,
19
- TextIteratorStreamer,
20
  )
21
 
22
  from gradio.themes import Soft
@@ -26,7 +25,6 @@ from gradio.themes.utils import colors, fonts, sizes
26
  # Character Error Rate (CER)
27
  # ============================================================
28
 
29
-
30
  def levenshtein(a: str, b: str) -> int:
31
  """Levenshtein distance to calculate CER."""
32
  a, b = a.lower(), b.lower()
@@ -63,7 +61,6 @@ from huggingface_hub import hf_hub_download
63
 
64
  REPO_ID = "IFMedTech/Medibot_OCR_model" # private backend repo
65
 
66
- # Filenames in the repo → class names they define
67
  PY_MODULES: Dict[str, str] = {
68
  "clinical_NER.py": "ClinicalNER",
69
  "tf_idf_phonetic.py": "TfidfPhoneticMatcher",
@@ -73,7 +70,6 @@ PY_MODULES: Dict[str, str] = {
73
 
74
  HF_TOKEN = os.environ.get("HUGGINGFACE_TOKEN") # must be set in Space secrets
75
 
76
-
77
  def _dynamic_import(module_path: str, class_name: str):
78
  spec = importlib.util.spec_from_file_location(class_name, module_path)
79
  module = importlib.util.module_from_spec(spec)
@@ -93,7 +89,7 @@ if HF_TOKEN is None:
93
  else:
94
  print(f"[Private] Using repo: {REPO_ID}")
95
 
96
- # 1) Load python modules (best-effort: failure of one file will not block others)
97
  for fname, cls_name in PY_MODULES.items():
98
  try:
99
  print(f"[Private] Downloading module file: {fname}")
@@ -120,8 +116,6 @@ else:
120
  repo_type="model",
121
  )
122
  print(f"[Private] Downloaded Excel at: {drug_xlsx_path}")
123
-
124
- # Debug: verify read
125
  df_debug = pd.read_excel(drug_xlsx_path, nrows=3)
126
  print(
127
  f"[Private] Excel loaded successfully. "
@@ -238,20 +232,17 @@ DTYPE_BF16 = torch.bfloat16 if use_cuda else torch.float32
238
  # ============================================================
239
  # OCR MODELS: Chandra-OCR + Dots.OCR
240
  # ============================================================
241
- # 1) Chandra-OCR (Qwen3VL)
242
  MODEL_ID_V = "datalab-to/chandra"
243
  processor_v = AutoProcessor.from_pretrained(MODEL_ID_V, trust_remote_code=True)
244
  model_v = Qwen3VLForConditionalGeneration.from_pretrained(
245
  MODEL_ID_V, trust_remote_code=True, torch_dtype=DTYPE_FP16
246
  ).to(device).eval()
247
 
248
- # 2) Dots.OCR (flash_attn2 if available, else SDPA)
249
  MODEL_PATH_D = "prithivMLmods/Dots.OCR-Latest-BF16"
250
  processor_d = AutoProcessor.from_pretrained(MODEL_PATH_D, trust_remote_code=True)
251
  attn_impl = "sdpa"
252
  try:
253
  import flash_attn # noqa: F401
254
-
255
  if use_cuda:
256
  attn_impl = "flash_attention_2"
257
  except Exception:
@@ -268,9 +259,7 @@ if not use_cuda:
268
  model_d.to(device)
269
 
270
  # ============================================================
271
- # GENERATION (OCR Med extraction Spell-check + CER)
272
- # ClinicalNER is used ONLY for Dots.OCR.
273
- # Single output: Markdown only (no raw stream exposed).
274
  # ============================================================
275
  MAX_MAX_NEW_TOKENS = 4096
276
  DEFAULT_MAX_NEW_TOKENS = 2048
@@ -287,229 +276,208 @@ def generate_image(
287
  top_k: int,
288
  repetition_penalty: float,
289
  spell_algo: str,
290
- ):
291
  """
292
  Returns a single Markdown string:
293
  - Medications (extracted)
294
  - Spell-check suggestions
295
  No raw OCR text is returned to the UI.
296
  """
297
- # Always return ONE value (Markdown string)
298
- if image is None:
299
- yield "Please upload an image."
300
- return
301
-
302
- # Choose processor/model
303
- if model_name == "Chandra-OCR":
304
- processor, model = processor_v, model_v
305
- elif model_name == "Dots.OCR":
306
- processor, model = processor_d, model_d
307
- else:
308
- yield "Invalid model selected."
309
- return
310
-
311
- # Prompt (text is provided via gr.State)
312
- messages = [
313
- {
314
- "role": "user",
315
- "content": [
316
- {"type": "image"},
317
- {"type": "text", "text": text},
318
- ],
319
- }
320
- ]
321
- prompt_full = processor.apply_chat_template(
322
- messages, tokenize=False, add_generation_prompt=True
323
- )
324
 
325
- # Preprocess
326
- inputs = processor(
327
- text=[prompt_full], images=[image], return_tensors="pt", padding=True
328
- )
329
- inputs = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in inputs.items()}
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- # Streamer
332
- tokenizer = getattr(processor, "tokenizer", None) or processor
333
- streamer = TextIteratorStreamer(
334
- tokenizer, skip_prompt=True, skip_special_tokens=True
335
- )
336
 
337
- gen_kwargs = dict(
338
- **inputs,
339
- streamer=streamer,
340
- max_new_tokens=max_new_tokens,
341
- do_sample=True,
342
- temperature=temperature,
343
- top_p=top_p,
344
- top_k=top_k,
345
- repetition_penalty=repetition_penalty,
346
- )
347
 
348
- # Start generation in background thread
349
- thread = Thread(target=model.generate, kwargs=gen_kwargs)
350
- thread.start()
351
-
352
- # 1) Live loop: we don't show raw text, just a "Processing..." placeholder once
353
- buffer = ""
354
- first = True
355
- for new_text in streamer:
356
- buffer += new_text.replace("<|im_end|>", "")
357
- if first:
358
- # Only one interim update to UI
359
- yield "Processing..."
360
- first = False
361
- time.sleep(0.01)
362
-
363
- final_ocr_text = buffer.strip()
364
-
365
- # --------------------------------------------------------
366
- # 2) Medications extraction
367
- # --------------------------------------------------------
368
- meds: List[str] = []
369
-
370
- if model_name == "Dots.OCR":
371
- # ClinicalNER ONLY for Dots.OCR
372
- try:
373
- if "ClinicalNER" in priv_classes and HF_TOKEN is not None:
374
- ClinicalNER = priv_classes["ClinicalNER"]
375
- ner = ClinicalNER(token=HF_TOKEN)
376
- ner_output = ner(final_ocr_text) or []
377
  meds = [
378
- m.strip()
379
- for m in ner_output
380
- if isinstance(m, str) and m.strip()
381
  ]
382
- print("[NER] (Dots.OCR) ClinicalNER meds:", meds)
383
- else:
384
- print("[NER] ClinicalNER unavailable or missing HF token; skipping.")
385
- except Exception as e:
386
- print(f"[NER] Error running ClinicalNER: {e}")
387
 
388
- # Fallback if ClinicalNER returns nothing
389
- if not meds:
390
  meds = [
391
  line.strip()
392
  for line in final_ocr_text.splitlines()
393
  if line.strip()
394
  ]
395
- print("[NER] (Dots.OCR) Fallback to lines, count:", len(meds))
396
-
397
- elif model_name == "Chandra-OCR":
398
- # NO ClinicalNER for Chandra; just use text lines
399
- meds = [
400
- line.strip()
401
- for line in final_ocr_text.splitlines()
402
- if line.strip()
403
- ]
404
- print("[NER] (Chandra-OCR) Line-based meds only, count:", len(meds))
405
-
406
- print("[DEBUG] meds count:", len(meds))
407
- print("[DEBUG] drug_xlsx_path in generate_image:", drug_xlsx_path)
408
-
409
- # --------------------------------------------------------
410
- # 3) Build Markdown base: Medications only (no Raw OCR)
411
- # --------------------------------------------------------
412
- md = "### Medications (extracted)\n"
413
- if meds:
414
- for m in meds:
415
- md += f"- {m}\n"
416
- else:
417
- md += "- None detected\n"
418
-
419
- # --------------------------------------------------------
420
- # 4) Spell-check (med list) with CER
421
- # --------------------------------------------------------
422
- spell_section = "\n---\n### Spell-check suggestions (" + spell_algo + ")\n"
423
- corr: Dict[str, List] = {}
424
-
425
- if BACKEND_INIT_ERROR:
426
- spell_section += f"- [DEBUG] Backend init error: {BACKEND_INIT_ERROR}\n"
427
 
428
- try:
429
- if meds and drug_xlsx_path:
430
- # Optional Excel debug read
431
- try:
432
- df_dbg = pd.read_excel(drug_xlsx_path)
433
- print(
434
- f"[Spell DEBUG] Excel read OK: path={drug_xlsx_path}, "
435
- f"shape={df_dbg.shape}, cols={list(df_dbg.columns)}"
436
- )
437
- spell_section += (
438
- f"- [DEBUG] Excel read OK; shape={df_dbg.shape}, "
439
- f"cols={list(df_dbg.columns)}\n"
440
- )
441
- except Exception as e:
442
- print(f"[Spell DEBUG] ERROR reading Excel in generate_image: {e}")
443
- spell_section += f"- [DEBUG] Excel read error: {e}\n"
444
-
445
- # Pick matcher based on spell_algo
446
- if (
447
- spell_algo == "TF-IDF + Phonetic"
448
- and "TfidfPhoneticMatcher" in priv_classes
449
- ):
450
- print("[Spell DEBUG] Using TfidfPhoneticMatcher")
451
- Cls = priv_classes["TfidfPhoneticMatcher"]
452
- checker = Cls(
453
- xlsx_path=drug_xlsx_path,
454
- column="Combined_Drugs",
455
- ngram_size=3,
456
- phonetic_weight=0.4,
457
- )
458
- corr = checker.match_list(meds, top_k=5, tfidf_threshold=0.15)
459
-
460
- elif spell_algo == "SymSpell" and "SymSpellMatcher" in priv_classes:
461
- print("[Spell DEBUG] Using SymSpellMatcher")
462
- Cls = priv_classes["SymSpellMatcher"]
463
- checker = Cls(
464
- xlsx_path=drug_xlsx_path,
465
- column="Combined_Drugs",
466
- max_edit=2,
467
- prefix_len=7,
468
- )
469
- corr = checker.match_list(meds, top_k=5, min_score=0.4)
470
 
471
- elif spell_algo == "RapidFuzz" and "RapidFuzzMatcher" in priv_classes:
472
- print("[Spell DEBUG] Using RapidFuzzMatcher")
473
- Cls = priv_classes["RapidFuzzMatcher"]
474
- checker = Cls(xlsx_path=drug_xlsx_path, column="Combined_Drugs")
475
- corr = checker.match_list(meds, top_k=5, threshold=70.0)
476
 
477
- else:
478
- spell_section += (
479
- "- Spell-check backend unavailable "
480
- "(no matcher class for selected algorithm).\n"
481
- )
482
- else:
483
- if not meds:
484
- spell_section += "- No medications extracted (empty med list).\n"
485
- if not drug_xlsx_path:
486
- spell_section += (
487
- "- Drug Excel dictionary path missing "
488
- "(drug_xlsx_path is None).\n"
489
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
- except Exception as e:
492
- print(f"[Spell DEBUG] Spell-check error: {e}")
493
- spell_section += f"- Spell-check error: {e}\n"
494
-
495
- # Format suggestions (top-5 per med, with scores + CER)
496
- if corr:
497
- for raw in meds:
498
- suggestions = corr.get(raw, [])
499
- if suggestions:
500
- spell_section += f"- **{raw}**\n"
501
- for cand, score in suggestions:
502
- cer = character_error_rate(cand, raw)
503
  spell_section += (
504
- f" - {cand} (score={score:.3f}, CER={cer:.3f}%)\n"
 
505
  )
506
  else:
507
- spell_section += f"- **{raw}**\n - (no suggestions)\n"
 
 
 
 
 
 
508
 
509
- final_md = md + spell_section
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
- # Final yield: SINGLE markdown string
512
- yield final_md
 
 
 
 
513
 
514
 
515
  # ============================================================
@@ -620,8 +588,6 @@ if __name__ == "__main__":
620
 
621
 
622
 
623
-
624
-
625
  ###################################### version 4 #########################################
626
 
627
 
 
2
 
3
 
4
  import os
 
 
5
  from typing import Iterable, Dict, Any, Optional, List
6
+ from threading import Thread # no longer needed but harmless if left
7
+ import time # no longer needed but harmless if left
8
 
9
  import gradio as gr
10
  import spaces
11
  import torch
12
  from PIL import Image
13
+ import pandas as pd
14
 
15
  from transformers import (
16
  Qwen3VLForConditionalGeneration,
17
  AutoModelForCausalLM,
18
  AutoProcessor,
 
19
  )
20
 
21
  from gradio.themes import Soft
 
25
  # Character Error Rate (CER)
26
  # ============================================================
27
 
 
28
  def levenshtein(a: str, b: str) -> int:
29
  """Levenshtein distance to calculate CER."""
30
  a, b = a.lower(), b.lower()
 
61
 
62
  REPO_ID = "IFMedTech/Medibot_OCR_model" # private backend repo
63
 
 
64
  PY_MODULES: Dict[str, str] = {
65
  "clinical_NER.py": "ClinicalNER",
66
  "tf_idf_phonetic.py": "TfidfPhoneticMatcher",
 
70
 
71
  HF_TOKEN = os.environ.get("HUGGINGFACE_TOKEN") # must be set in Space secrets
72
 
 
73
  def _dynamic_import(module_path: str, class_name: str):
74
  spec = importlib.util.spec_from_file_location(class_name, module_path)
75
  module = importlib.util.module_from_spec(spec)
 
89
  else:
90
  print(f"[Private] Using repo: {REPO_ID}")
91
 
92
+ # 1) Load python modules (best-effort)
93
  for fname, cls_name in PY_MODULES.items():
94
  try:
95
  print(f"[Private] Downloading module file: {fname}")
 
116
  repo_type="model",
117
  )
118
  print(f"[Private] Downloaded Excel at: {drug_xlsx_path}")
 
 
119
  df_debug = pd.read_excel(drug_xlsx_path, nrows=3)
120
  print(
121
  f"[Private] Excel loaded successfully. "
 
232
  # ============================================================
233
  # OCR MODELS: Chandra-OCR + Dots.OCR
234
  # ============================================================
 
235
  MODEL_ID_V = "datalab-to/chandra"
236
  processor_v = AutoProcessor.from_pretrained(MODEL_ID_V, trust_remote_code=True)
237
  model_v = Qwen3VLForConditionalGeneration.from_pretrained(
238
  MODEL_ID_V, trust_remote_code=True, torch_dtype=DTYPE_FP16
239
  ).to(device).eval()
240
 
 
241
  MODEL_PATH_D = "prithivMLmods/Dots.OCR-Latest-BF16"
242
  processor_d = AutoProcessor.from_pretrained(MODEL_PATH_D, trust_remote_code=True)
243
  attn_impl = "sdpa"
244
  try:
245
  import flash_attn # noqa: F401
 
246
  if use_cuda:
247
  attn_impl = "flash_attention_2"
248
  except Exception:
 
259
  model_d.to(device)
260
 
261
  # ============================================================
262
+ # GENERATION (no raw output UI; one markdown return)
 
 
263
  # ============================================================
264
  MAX_MAX_NEW_TOKENS = 4096
265
  DEFAULT_MAX_NEW_TOKENS = 2048
 
276
  top_k: int,
277
  repetition_penalty: float,
278
  spell_algo: str,
279
+ ) -> str:
280
  """
281
  Returns a single Markdown string:
282
  - Medications (extracted)
283
  - Spell-check suggestions
284
  No raw OCR text is returned to the UI.
285
  """
286
+ try:
287
+ if image is None:
288
+ return "Please upload an image."
289
+
290
+ # Choose processor/model
291
+ if model_name == "Chandra-OCR":
292
+ processor, model = processor_v, model_v
293
+ elif model_name == "Dots.OCR":
294
+ processor, model = processor_d, model_d
295
+ else:
296
+ return "Invalid model selected."
297
+
298
+ # Build prompt
299
+ messages = [
300
+ {
301
+ "role": "user",
302
+ "content": [
303
+ {"type": "image"},
304
+ {"type": "text", "text": text},
305
+ ],
306
+ }
307
+ ]
308
+ prompt_full = processor.apply_chat_template(
309
+ messages, tokenize=False, add_generation_prompt=True
310
+ )
 
 
311
 
312
+ # Preprocess
313
+ inputs = processor(
314
+ text=[prompt_full], images=[image], return_tensors="pt", padding=True
315
+ )
316
+ inputs = {k: (v.to(device) if hasattr(v, "to") else v) for k, v in inputs.items()}
317
+
318
+ # Generate (no streaming)
319
+ gen_kwargs = dict(
320
+ **inputs,
321
+ max_new_tokens=max_new_tokens,
322
+ do_sample=True,
323
+ temperature=temperature,
324
+ top_p=top_p,
325
+ top_k=top_k,
326
+ repetition_penalty=repetition_penalty,
327
+ )
328
+ outputs = model.generate(**gen_kwargs)
329
 
330
+ tokenizer = getattr(processor, "tokenizer", None) or processor
331
+ generated = tokenizer.batch_decode(outputs, skip_special_tokens=True)[0]
332
+ final_ocr_text = generated.strip()
 
 
333
 
334
+ # --------------------------------------------------------
335
+ # 2) Medications extraction
336
+ # --------------------------------------------------------
337
+ meds: List[str] = []
 
 
 
 
 
 
338
 
339
+ if model_name == "Dots.OCR":
340
+ try:
341
+ if "ClinicalNER" in priv_classes and HF_TOKEN is not None:
342
+ ClinicalNER = priv_classes["ClinicalNER"]
343
+ ner = ClinicalNER(token=HF_TOKEN)
344
+ ner_output = ner(final_ocr_text) or []
345
+ meds = [
346
+ m.strip()
347
+ for m in ner_output
348
+ if isinstance(m, str) and m.strip()
349
+ ]
350
+ print("[NER] (Dots.OCR) ClinicalNER meds:", meds)
351
+ else:
352
+ print("[NER] ClinicalNER unavailable or missing HF token; skipping.")
353
+ except Exception as e:
354
+ print(f"[NER] Error running ClinicalNER: {e}")
355
+
356
+ if not meds:
 
 
 
 
 
 
 
 
 
 
 
357
  meds = [
358
+ line.strip()
359
+ for line in final_ocr_text.splitlines()
360
+ if line.strip()
361
  ]
362
+ print("[NER] (Dots.OCR) Fallback to lines, count:", len(meds))
 
 
 
 
363
 
364
+ else: # Chandra-OCR
 
365
  meds = [
366
  line.strip()
367
  for line in final_ocr_text.splitlines()
368
  if line.strip()
369
  ]
370
+ print("[NER] (Chandra-OCR) Line-based meds only, count:", len(meds))
371
+
372
+ print("[DEBUG] meds count:", len(meds))
373
+ print("[DEBUG] drug_xlsx_path in generate_image:", drug_xlsx_path)
374
+
375
+ # --------------------------------------------------------
376
+ # 3) Markdown: Medications only (no Raw OCR section)
377
+ # --------------------------------------------------------
378
+ md = "### Medications (extracted)\n"
379
+ if meds:
380
+ for m in meds:
381
+ md += f"- {m}\n"
382
+ else:
383
+ md += "- None detected\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
+ # --------------------------------------------------------
386
+ # 4) Spell-check (med list) with CER
387
+ # --------------------------------------------------------
388
+ spell_section = "\n---\n### Spell-check suggestions (" + spell_algo + ")\n"
389
+ corr: Dict[str, List] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
+ if BACKEND_INIT_ERROR:
392
+ spell_section += f"- [DEBUG] Backend init error: {BACKEND_INIT_ERROR}\n"
 
 
 
393
 
394
+ try:
395
+ if meds and drug_xlsx_path:
396
+ try:
397
+ df_dbg = pd.read_excel(drug_xlsx_path)
398
+ print(
399
+ f"[Spell DEBUG] Excel read OK: path={drug_xlsx_path}, "
400
+ f"shape={df_dbg.shape}, cols={list(df_dbg.columns)}"
401
+ )
402
+ spell_section += (
403
+ f"- [DEBUG] Excel read OK; shape={df_dbg.shape}, "
404
+ f"cols={list(df_dbg.columns)}\n"
405
+ )
406
+ except Exception as e:
407
+ print(f"[Spell DEBUG] ERROR reading Excel in generate_image: {e}")
408
+ spell_section += f"- [DEBUG] Excel read error: {e}\n"
409
+
410
+ if (
411
+ spell_algo == "TF-IDF + Phonetic"
412
+ and "TfidfPhoneticMatcher" in priv_classes
413
+ ):
414
+ print("[Spell DEBUG] Using TfidfPhoneticMatcher")
415
+ Cls = priv_classes["TfidfPhoneticMatcher"]
416
+ checker = Cls(
417
+ xlsx_path=drug_xlsx_path,
418
+ column="Combined_Drugs",
419
+ ngram_size=3,
420
+ phonetic_weight=0.4,
421
+ )
422
+ corr = checker.match_list(meds, top_k=5, tfidf_threshold=0.15)
423
+
424
+ elif spell_algo == "SymSpell" and "SymSpellMatcher" in priv_classes:
425
+ print("[Spell DEBUG] Using SymSpellMatcher")
426
+ Cls = priv_classes["SymSpellMatcher"]
427
+ checker = Cls(
428
+ xlsx_path=drug_xlsx_path,
429
+ column="Combined_Drugs",
430
+ max_edit=2,
431
+ prefix_len=7,
432
+ )
433
+ corr = checker.match_list(meds, top_k=5, min_score=0.4)
434
 
435
+ elif spell_algo == "RapidFuzz" and "RapidFuzzMatcher" in priv_classes:
436
+ print("[Spell DEBUG] Using RapidFuzzMatcher")
437
+ Cls = priv_classes["RapidFuzzMatcher"]
438
+ checker = Cls(xlsx_path=drug_xlsx_path, column="Combined_Drugs")
439
+ corr = checker.match_list(meds, top_k=5, threshold=70.0)
440
+
441
+ else:
 
 
 
 
 
442
  spell_section += (
443
+ "- Spell-check backend unavailable "
444
+ "(no matcher class for selected algorithm).\n"
445
  )
446
  else:
447
+ if not meds:
448
+ spell_section += "- No medications extracted (empty med list).\n"
449
+ if not drug_xlsx_path:
450
+ spell_section += (
451
+ "- Drug Excel dictionary path missing "
452
+ "(drug_xlsx_path is None).\n"
453
+ )
454
 
455
+ except Exception as e:
456
+ print(f"[Spell DEBUG] Spell-check error: {e}")
457
+ spell_section += f"- Spell-check error: {e}\n"
458
+
459
+ if corr:
460
+ for raw in meds:
461
+ suggestions = corr.get(raw, [])
462
+ if suggestions:
463
+ spell_section += f"- **{raw}**\n"
464
+ for cand, score in suggestions:
465
+ cer = character_error_rate(cand, raw)
466
+ spell_section += (
467
+ f" - {cand} (score={score:.3f}, CER={cer:.3f}%)\n"
468
+ )
469
+ else:
470
+ spell_section += f"- **{raw}**\n - (no suggestions)\n"
471
+
472
+ final_md = md + spell_section
473
+ return final_md
474
 
475
+ except Exception as e:
476
+ # Catch-all so the GPU worker does not crash
477
+ print(f"[ERROR] generate_image crashed: {e}")
478
+ import traceback
479
+ traceback.print_exc()
480
+ return f"Error while processing: {e}"
481
 
482
 
483
  # ============================================================
 
588
 
589
 
590
 
 
 
591
  ###################################### version 4 #########################################
592
 
593