Ankushbl6 commited on
Commit
8b9a0d2
Β·
verified Β·
1 Parent(s): 98f5b15

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +123 -117
src/streamlit_app.py CHANGED
@@ -262,9 +262,10 @@ col1, col2 = st.columns([3, 2])
262
  display_field_name = SINGLE_FIELDS[0]
263
  storage_field_name = SINGLE_FIELDS[0]
264
  base_field_for_color = SINGLE_FIELDS[0]
 
265
 
 
266
  with col2:
267
- # ========== FIELD SELECTION (RHS) ==========
268
  st.markdown("#### 🎯 Field Selection")
269
 
270
  def add_line_item():
@@ -293,7 +294,6 @@ with col2:
293
  else:
294
  num_items = st.session_state.num_line_items[selected_name]
295
 
296
- # Single level of columns inside col2 (no deeper nesting)
297
  line_col1, add_col, rem_col = st.columns([2, 1, 1])
298
  with line_col1:
299
  line_item_options = [f"Line {i+1}" for i in range(num_items)]
@@ -311,14 +311,15 @@ with col2:
311
  storage_field_name = f"Line {line_item_num}: {base_field}"
312
  base_field_for_color = base_field
313
 
314
- # Guard in case something weird happens
315
  if not storage_field_name:
316
  storage_field_name = display_field_name
317
 
318
  field_color = FIELD_COLORS.get(base_field_for_color or display_field_name, "#FF0000")
319
- st.markdown(f"**Current:** <span style='color:{field_color}'>●</span> {display_field_name}", unsafe_allow_html=True)
 
 
 
320
 
321
- # ========== ZOOM CONTROLS (RHS) ==========
322
  st.markdown("#### πŸ” Zoom")
323
 
324
  current_zoom = st.session_state.zoom_values[selected_name]
@@ -369,115 +370,13 @@ with col2:
369
 
370
  st.caption(f"Original: {pil_image.width}Γ—{pil_image.height}")
371
 
372
- # ========== OCR & VALUE SECTION ==========
373
- st.markdown("#### ✏️ OCR & Value")
374
-
375
- current_rect_orig = st.session_state.field_rects_orig[selected_name].get(storage_field_name)
376
- current_val = st.session_state.field_values[selected_name].get(storage_field_name, "")
377
-
378
- new_val = st.text_area(
379
- "Value (auto-filled by OCR)",
380
- value=current_val,
381
- height=80,
382
- label_visibility="collapsed",
383
- placeholder="Value (auto-filled by OCR)",
384
- )
385
-
386
- col_btn1, col_btn2, col_btn3 = st.columns(3)
387
-
388
- with col_btn1:
389
- if st.button("πŸ’Ύ Save"):
390
- st.session_state.field_values[selected_name][storage_field_name] = new_val
391
- st.success("Saved!")
392
-
393
- with col_btn2:
394
- if current_rect_orig and st.button("πŸ”„ Re-OCR"):
395
- x1 = max(0, int(current_rect_orig["left"]))
396
- y1 = max(0, int(current_rect_orig["top"]))
397
- x2 = min(pil_image.width, int(current_rect_orig["left"] + current_rect_orig["width"]))
398
- y2 = min(pil_image.height, int(current_rect_orig["top"] + current_rect_orig["height"]))
399
- if x2 > x1 and y2 > y1:
400
- crop = pil_image.crop((x1, y1, x2, y2))
401
- try:
402
- text = pytesseract.image_to_string(crop, config="--psm 6").strip()
403
- if text:
404
- st.session_state.field_values[selected_name][storage_field_name] = text
405
- st.success(f"OCR: {text}")
406
- else:
407
- st.warning("Empty result")
408
- except Exception as e:
409
- st.error(f"OCR failed: {e}")
410
- # force rerun so text area picks up new value
411
- st.experimental_rerun()
412
-
413
- with col_btn3:
414
- def delete_rect():
415
- st.session_state.pending_delete = (selected_name, storage_field_name)
416
-
417
- if current_rect_orig:
418
- st.button("πŸ—‘οΈ Delete", on_click=delete_rect)
419
-
420
- # ========== ALL VALUES SECTION ==========
421
- with st.expander("πŸ“‹ All Values"):
422
- for f in SINGLE_FIELDS:
423
- v = st.session_state.field_values[selected_name].get(f, "")
424
- if v.strip():
425
- st.write(f"**{f}:** {v}")
426
- num_items = st.session_state.num_line_items[selected_name]
427
- for i in range(1, num_items + 1):
428
- vals = [(lif, st.session_state.field_values[selected_name].get(f"Line {i}: {lif}", ""))
429
- for lif in LINE_ITEM_FIELDS]
430
- vals = [(lif, v) for lif, v in vals if v.strip()]
431
- if vals:
432
- st.write(f"**Line {i}:**")
433
- for lif, v in vals:
434
- st.write(f" {lif}: {v}")
435
-
436
- # ========== EXPORT SECTION ==========
437
- st.markdown("#### πŸ“€ JSONL Export")
438
-
439
- # Export ALL labeled remittances
440
- records_all = [
441
- build_gt_record_for_file(img["name"])
442
- for img in images
443
- if has_any_label(img["name"])
444
- ]
445
-
446
- if records_all:
447
- all_jsonl_str = "\n".join(
448
- json.dumps(rec, ensure_ascii=False) for rec in records_all
449
- )
450
- st.download_button(
451
- "⬇️ Export ALL labeled (JSONL)",
452
- data=all_jsonl_str.encode("utf-8"),
453
- file_name="remittances_ground_truth.jsonl",
454
- mime="application/json",
455
- )
456
- else:
457
- st.caption("No labeled remittances yet.")
458
-
459
- # Export CURRENT remittance
460
- current_record = build_gt_record_for_file(selected_name)
461
- with st.expander("Preview CURRENT JSON"):
462
- st.json(current_record)
463
-
464
- current_jsonl_str = json.dumps(current_record, ensure_ascii=False) + "\n"
465
- st.download_button(
466
- "⬇️ Export CURRENT (JSONL)",
467
- data=current_jsonl_str.encode("utf-8"),
468
- file_name=f"{os.path.splitext(selected_name)[0]}_remittance.jsonl",
469
- mime="application/json",
470
- )
471
-
472
  with col1:
473
- # ========== CANVAS / IMAGE (LEFT SIDE) ==========
474
  zoom = st.session_state.zoom_values[selected_name]
475
-
476
  scale = zoom / 100.0
477
  disp_w = int(pil_image.width * scale)
478
  disp_h = int(pil_image.height * scale)
479
 
480
- # Get display image - fresh PIL object each time from stable bytes
481
  display_image = get_display_image_from_bytes(image_bytes, disp_w, disp_h)
482
 
483
  def orig_to_display(rect_orig, s):
@@ -518,12 +417,10 @@ with col1:
518
  initial_drawing = {"version": "4.4.0", "objects": all_display_objects}
519
  expected_count = len(all_display_objects)
520
 
521
- # Canvas key: includes rect count to force refresh when rectangles change
522
  rect_ver = st.session_state.rect_version[selected_name]
523
  num_rects = len(st.session_state.field_rects_orig[selected_name])
524
  canvas_key = f"canvas_{selected_name}_z{zoom}_rv{rect_ver}_n{num_rects}"
525
 
526
- # Render canvas
527
  canvas_result = st_canvas(
528
  background_image=display_image,
529
  height=disp_h,
@@ -537,29 +434,32 @@ with col1:
537
  key=canvas_key,
538
  )
539
 
540
- # Detect new rectangle
541
  if canvas_result.json_data is not None:
542
  objs = canvas_result.json_data.get("objects", [])
543
  if len(objs) > expected_count:
544
  new_rect_display = objs[-1]
545
  new_rect_orig = display_to_orig(new_rect_display, scale)
546
  new_rect_orig["stroke"] = field_color
 
 
547
  st.session_state.field_rects_orig[selected_name][storage_field_name] = new_rect_orig
548
- # bump rect version so canvas resets to only newest rect for this field
549
  st.session_state.rect_version[selected_name] += 1
550
-
551
- # Auto-run OCR
552
  x1 = max(0, int(new_rect_orig["left"]))
553
  y1 = max(0, int(new_rect_orig["top"]))
554
  x2 = min(pil_image.width, int(new_rect_orig["left"] + new_rect_orig["width"]))
555
  y2 = min(pil_image.height, int(new_rect_orig["top"] + new_rect_orig["height"]))
556
-
557
  if x2 > x1 and y2 > y1:
558
  crop = pil_image.crop((x1, y1, x2, y2))
559
  try:
560
  text = pytesseract.image_to_string(crop, config="--psm 6").strip()
561
  if text:
 
562
  st.session_state.field_values[selected_name][storage_field_name] = text
 
 
563
  st.toast(f"βœ… OCR: {text[:50]}{'...' if len(text) > 50 else ''}")
564
  else:
565
  st.toast("βœ… Rectangle saved (no text detected)")
@@ -567,5 +467,111 @@ with col1:
567
  st.toast("βœ… Rectangle saved")
568
  else:
569
  st.toast("βœ… Rectangle saved")
570
- # force rerun so RHS text box shows new OCR value
571
- st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  display_field_name = SINGLE_FIELDS[0]
263
  storage_field_name = SINGLE_FIELDS[0]
264
  base_field_for_color = SINGLE_FIELDS[0]
265
+ field_color = FIELD_COLORS[base_field_for_color]
266
 
267
+ # ====== RHS TOP: FIELD SELECTION + ZOOM ======
268
  with col2:
 
269
  st.markdown("#### 🎯 Field Selection")
270
 
271
  def add_line_item():
 
294
  else:
295
  num_items = st.session_state.num_line_items[selected_name]
296
 
 
297
  line_col1, add_col, rem_col = st.columns([2, 1, 1])
298
  with line_col1:
299
  line_item_options = [f"Line {i+1}" for i in range(num_items)]
 
311
  storage_field_name = f"Line {line_item_num}: {base_field}"
312
  base_field_for_color = base_field
313
 
 
314
  if not storage_field_name:
315
  storage_field_name = display_field_name
316
 
317
  field_color = FIELD_COLORS.get(base_field_for_color or display_field_name, "#FF0000")
318
+ st.markdown(
319
+ f"**Current:** <span style='color:{field_color}'>●</span> {display_field_name}",
320
+ unsafe_allow_html=True,
321
+ )
322
 
 
323
  st.markdown("#### πŸ” Zoom")
324
 
325
  current_zoom = st.session_state.zoom_values[selected_name]
 
370
 
371
  st.caption(f"Original: {pil_image.width}Γ—{pil_image.height}")
372
 
373
+ # ====== LHS: CANVAS / IMAGE ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  with col1:
 
375
  zoom = st.session_state.zoom_values[selected_name]
 
376
  scale = zoom / 100.0
377
  disp_w = int(pil_image.width * scale)
378
  disp_h = int(pil_image.height * scale)
379
 
 
380
  display_image = get_display_image_from_bytes(image_bytes, disp_w, disp_h)
381
 
382
  def orig_to_display(rect_orig, s):
 
417
  initial_drawing = {"version": "4.4.0", "objects": all_display_objects}
418
  expected_count = len(all_display_objects)
419
 
 
420
  rect_ver = st.session_state.rect_version[selected_name]
421
  num_rects = len(st.session_state.field_rects_orig[selected_name])
422
  canvas_key = f"canvas_{selected_name}_z{zoom}_rv{rect_ver}_n{num_rects}"
423
 
 
424
  canvas_result = st_canvas(
425
  background_image=display_image,
426
  height=disp_h,
 
434
  key=canvas_key,
435
  )
436
 
437
+ # Detect new rectangle and auto-OCR
438
  if canvas_result.json_data is not None:
439
  objs = canvas_result.json_data.get("objects", [])
440
  if len(objs) > expected_count:
441
  new_rect_display = objs[-1]
442
  new_rect_orig = display_to_orig(new_rect_display, scale)
443
  new_rect_orig["stroke"] = field_color
444
+
445
+ # overwrite previous rect for this field
446
  st.session_state.field_rects_orig[selected_name][storage_field_name] = new_rect_orig
 
447
  st.session_state.rect_version[selected_name] += 1
448
+
 
449
  x1 = max(0, int(new_rect_orig["left"]))
450
  y1 = max(0, int(new_rect_orig["top"]))
451
  x2 = min(pil_image.width, int(new_rect_orig["left"] + new_rect_orig["width"]))
452
  y2 = min(pil_image.height, int(new_rect_orig["top"] + new_rect_orig["height"]))
453
+
454
  if x2 > x1 and y2 > y1:
455
  crop = pil_image.crop((x1, y1, x2, y2))
456
  try:
457
  text = pytesseract.image_to_string(crop, config="--psm 6").strip()
458
  if text:
459
+ # update field values and the text-area state key
460
  st.session_state.field_values[selected_name][storage_field_name] = text
461
+ value_state_key = f"value_{selected_name}_{storage_field_name}"
462
+ st.session_state[value_state_key] = text
463
  st.toast(f"βœ… OCR: {text[:50]}{'...' if len(text) > 50 else ''}")
464
  else:
465
  st.toast("βœ… Rectangle saved (no text detected)")
 
467
  st.toast("βœ… Rectangle saved")
468
  else:
469
  st.toast("βœ… Rectangle saved")
470
+
471
+ # ====== RHS BOTTOM: OCR VALUE + EXPORT ======
472
+ with col2:
473
+ st.markdown("#### ✏️ OCR & Value")
474
+
475
+ current_rect_orig = st.session_state.field_rects_orig[selected_name].get(storage_field_name)
476
+ value_state_key = f"value_{selected_name}_{storage_field_name}"
477
+
478
+ # initialise text state from saved field value on first use
479
+ if value_state_key not in st.session_state:
480
+ st.session_state[value_state_key] = st.session_state.field_values[selected_name].get(
481
+ storage_field_name, ""
482
+ )
483
+
484
+ col_btn1, col_btn2, col_btn3 = st.columns(3)
485
+
486
+ with col_btn1:
487
+ if st.button("πŸ’Ύ Save"):
488
+ st.session_state.field_values[selected_name][storage_field_name] = st.session_state[value_state_key]
489
+ st.success("Saved!")
490
+
491
+ with col_btn2:
492
+ if current_rect_orig and st.button("πŸ”„ Re-OCR"):
493
+ x1 = max(0, int(current_rect_orig["left"]))
494
+ y1 = max(0, int(current_rect_orig["top"]))
495
+ x2 = min(pil_image.width, int(current_rect_orig["left"] + current_rect_orig["width"]))
496
+ y2 = min(pil_image.height, int(current_rect_orig["top"] + current_rect_orig["height"]))
497
+ if x2 > x1 and y2 > y1:
498
+ crop = pil_image.crop((x1, y1, x2, y2))
499
+ try:
500
+ text = pytesseract.image_to_string(crop, config="--psm 6").strip()
501
+ if text:
502
+ st.session_state.field_values[selected_name][storage_field_name] = text
503
+ st.session_state[value_state_key] = text
504
+ st.success(f"OCR: {text}")
505
+ else:
506
+ st.warning("Empty result")
507
+ except Exception as e:
508
+ st.error(f"OCR failed: {e}")
509
+
510
+ with col_btn3:
511
+ def delete_rect():
512
+ st.session_state.pending_delete = (selected_name, storage_field_name)
513
+ if current_rect_orig:
514
+ st.button("πŸ—‘οΈ Delete", on_click=delete_rect)
515
+
516
+ # Text area bound to state key (so it reflects auto-OCR & Re-OCR without reruns)
517
+ st.text_area(
518
+ "Value (auto-filled by OCR)",
519
+ key=value_state_key,
520
+ height=80,
521
+ label_visibility="collapsed",
522
+ placeholder="Value (auto-filled by OCR)",
523
+ )
524
+
525
+ # ========== ALL VALUES SECTION ==========
526
+ with st.expander("πŸ“‹ All Values"):
527
+ for f in SINGLE_FIELDS:
528
+ v = st.session_state.field_values[selected_name].get(f, "")
529
+ if v.strip():
530
+ st.write(f"**{f}:** {v}")
531
+ num_items = st.session_state.num_line_items[selected_name]
532
+ for i in range(1, num_items + 1):
533
+ vals = [
534
+ (lif, st.session_state.field_values[selected_name].get(f"Line {i}: {lif}", ""))
535
+ for lif in LINE_ITEM_FIELDS
536
+ ]
537
+ vals = [(lif, v) for lif, v in vals if v.strip()]
538
+ if vals:
539
+ st.write(f"**Line {i}:**")
540
+ for lif, v in vals:
541
+ st.write(f" {lif}: {v}")
542
+
543
+ # ========== EXPORT SECTION ==========
544
+ st.markdown("#### πŸ“€ JSONL Export")
545
+
546
+ # Export ALL labeled remittances
547
+ records_all = [
548
+ build_gt_record_for_file(img["name"])
549
+ for img in images
550
+ if has_any_label(img["name"])
551
+ ]
552
+
553
+ if records_all:
554
+ all_jsonl_str = "\n".join(
555
+ json.dumps(rec, ensure_ascii=False) for rec in records_all
556
+ )
557
+ st.download_button(
558
+ "⬇️ Export ALL labeled (JSONL)",
559
+ data=all_jsonl_str.encode("utf-8"),
560
+ file_name="remittances_ground_truth.jsonl",
561
+ mime="application/json",
562
+ )
563
+ else:
564
+ st.caption("No labeled remittances yet.")
565
+
566
+ # Export CURRENT remittance
567
+ current_record = build_gt_record_for_file(selected_name)
568
+ with st.expander("Preview CURRENT JSON"):
569
+ st.json(current_record)
570
+
571
+ current_jsonl_str = json.dumps(current_record, ensure_ascii=False) + "\n"
572
+ st.download_button(
573
+ "⬇️ Export CURRENT (JSONL)",
574
+ data=current_jsonl_str.encode("utf-8"),
575
+ file_name=f"{os.path.splitext(selected_name)[0]}_remittance.jsonl",
576
+ mime="application/json",
577
+ )