File size: 5,948 Bytes
c8716b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# -*- coding: utf-8 -*-
"""
render.py
ํŒ์ • ๊ฒฐ๊ณผ โ†’ ๊ฒ€์‚ฌ๊ด€์šฉ ์ถœ๋ ฅ (๋Œ€ํ‘œ ์Šฌ๋ผ์ด์Šค ์˜ค๋ฒ„๋ ˆ์ด + ๋ณด๊ณ ์„œ + RAG ์กฐ์น˜ ์ง€์นจ).

ํ‘œ์‹œ ๊ทœ์น™(๋ฐ‘์— ๊น”๋ฆฐ ๋ชจ๋ธ์ด ๋‹ฌ๋ผ์„œ):
  - ๋‹ค๊ณต์„ฑ(porosity) : seg ๋งˆ์Šคํฌ โ†’ ๋นจ๊ฐ• ๋ฐ˜ํˆฌ๋ช… (๋Œ€ํ‘œ ์Šฌ๋ผ์ด์Šค์—๋งŒ lazy ๊ณ„์‚ฐ)
  - ๋ ˆ์ง„(resin)      : ๊ฒ€์ถœ bbox โ†’ ๋นจ๊ฐ• ์‚ฌ๊ฐํ˜•
  - ํŒฝ์ฐฝ(swelling)   : ์œ„์น˜ ์—†์Œ(๋ถ„๋ฅ˜๊ธฐ) โ†’ ํ‘œ์‹œ ์•ˆ ํ•จ

์ขŒํ‘œ๊ณ„: ๊ฒ€์ถœ ๋ฐ•์Šค/ํ‘œ์‹œ ์ด๋ฏธ์ง€ ๋ชจ๋‘ 'recipe ํฌ๋กญ' ์ขŒํ‘œ๊ณ„๋ผ ๊ทธ๋Œ€๋กœ ๊ฒน์ณ์ง„๋‹ค.
"""
import os
import sys

import cv2
import numpy as np

import recipe
from decide import DEFECT_CANON
from data import open_slice
from seg_tiles import seg_porosity_mask

# generate_guidance.py(RAG) ์œ„์น˜๋ฅผ ๋ ˆ์ด์•„์›ƒ์— ๋ฌด๊ด€ํ•˜๊ฒŒ ํƒ์ƒ‰.
#   ๋กœ์ปฌ: ../teammate/rag,  HF Space: ./rag,  env GLUE_RAG_DIR ์šฐ์„ .
def _find_rag_dir():
    here = os.path.dirname(os.path.abspath(__file__))
    cands = [os.environ.get("GLUE_RAG_DIR"),
             os.path.join(here, "rag"),
             os.path.join(here, "teammate", "rag"),
             os.path.join(os.path.dirname(here), "teammate", "rag")]
    for c in cands:
        if c and os.path.exists(os.path.join(c, "generate_guidance.py")):
            return c
    return cands[-1]

_RAG_DIR = _find_rag_dir()
if _RAG_DIR not in sys.path:
    sys.path.insert(0, _RAG_DIR)


# ----------------------------------------------------------------------
# ๋Œ€ํ‘œ ์Šฌ๋ผ์ด์Šค ์„ ํƒ + ํฌ๋กญ ์ด๋ฏธ์ง€
# ----------------------------------------------------------------------
def _crop_rgb(slices, idx):
    """idx ์Šฌ๋ผ์ด์Šค์˜ ํฌ๋กญ ์ด๋ฏธ์ง€(๊ฒ€์ถœ/ํ‘œ์‹œ ๊ณตํ†ต ์ขŒํ‘œ๊ณ„) โ†’ RGB ndarray."""
    sl = next((s for s in slices if s["idx"] == idx), None)
    if sl is None:
        return None
    return np.array(recipe.to_rgb(open_slice(sl)))   # ์ด๋ฏธ ํฌ๋กญ๋จ


def pick_slice(result, slices):
    """๋Œ€ํ‘œ ์Šฌ๋ผ์ด์Šค = ๊ฒ€์ถœ ์‹ ๋ขฐ๋„๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ์Šฌ๋ผ์ด์Šค.
    ๊ฒ€์ถœ ๋ฐ•์Šค๊ฐ€ ์—†์œผ๋ฉด(์˜ˆ: swelling ๋‹จ๋…) (None, None) โ†’ ์œ„์น˜ ํ‘œ์‹œ ์ƒ๋žต.
    ๋ฐ˜ํ™˜: (slice_idx, crop_rgb ndarray)."""
    best_idx, best_conf = None, -1.0
    for box in result["porosity"]["boxes"] + result["resin"]["boxes"]:
        cf, si = box[4], int(box[5])
        if cf > best_conf:
            best_conf, best_idx = cf, si
    if best_idx is None:
        return None, None
    return best_idx, _crop_rgb(slices, best_idx)


def _blend_mask(img, mask, color=(255, 0, 0), alpha=0.45):
    """๋งˆ์Šคํฌ ์˜์—ญ ๋ฐ˜ํˆฌ๋ช… ์ƒ‰(RGB)."""
    overlay = img.copy()
    overlay[mask.astype(bool)] = color
    return cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0)


def make_overlay(crop_rgb, result, cur, seg_model=None):
    """๋Œ€ํ‘œ ์Šฌ๋ผ์ด์Šค(cur) ์œ„์— ๊ทธ ์Šฌ๋ผ์ด์Šค์˜ ๊ฒฐํ•จ๋งŒ ๊ทธ๋ฆฐ๋‹ค."""
    if crop_rgb is None:
        return None
    img = crop_rgb.copy()

    # ๋‹ค๊ณต์„ฑ โ†’ seg ๋งˆ์Šคํฌ(์ด ํฌ๋กญ์—๋งŒ lazy). ์‹คํŒจ ์‹œ ๊ฒ€์ถœ ๋ฐ•์Šค๋กœ ํด๋ฐฑ.
    drew_mask = False
    if seg_model is not None and result["porosity"]["flag"]:
        try:
            mask, _ = seg_porosity_mask(seg_model, crop_rgb, result["cell_type"])
            if mask.any():
                img = _blend_mask(img, mask)
                drew_mask = True
        except Exception as e:
            print(f"[seg ์ƒ๋žต] {e}", file=sys.stderr)
    if not drew_mask:
        for x1, y1, x2, y2, cf, si in result["porosity"]["boxes"]:
            if si == cur:
                cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 0), 2)

    # ๋ ˆ์ง„ โ†’ ๋ฐ•์Šค
    for x1, y1, x2, y2, cf, si in result["resin"]["boxes"]:
        if si == cur:
            cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (255, 128, 0), 2)
    # ํŒฝ์ฐฝ โ†’ ํ‘œ์‹œ ์—†์Œ
    return img


# ----------------------------------------------------------------------
# ๋ณด๊ณ ์„œ + RAG ์กฐ์น˜ ์ง€์นจ
# ----------------------------------------------------------------------
def make_report(result, decision):
    sw = result["swelling"]
    return {
        "๋ฐฐํ„ฐ๋ฆฌ":     result["battery_id"],
        "์…€ ํ˜•ํƒœ":    result["cell_type"],
        "ํŒ์ •":       decision["zone"],
        "๊ฒฐํ•จ ์ข…๋ฅ˜":  [DEFECT_CANON[d] for d in decision["defects"]] or ["์—†์Œ"],
        "์ตœ๊ณ  ์‹ ๋ขฐ๋„": round(decision["worst_conf"], 3),
        "swelling ๋น„์œจ": f"{sw['ratio']*100:.0f}% ({sw['n_swell']}/{result['n_slices']})",
        "์‚ฌ์œ ":       decision["reasons"],
    }


def recommend(result, decision):
    """RAG(generate_guidance) โ†’ IATA ๊ทœ์ • ๊ธฐ๋ฐ˜ ์กฐ์น˜ ์ง€์นจ(ํ•œ/์˜).
    ๊ฒฐํ•จ ์—†์œผ๋ฉด '์ด์ƒ์—†์Œ' ํ‘œ๊ธฐ(API ํ˜ธ์ถœ ์—†์Œ). ํ‚ค/๋„คํŠธ์›Œํฌ ์—†์œผ๋ฉด ๊ฒฐ์ •์  ๋งคํ•‘์œผ๋กœ ํด๋ฐฑ."""
    import generate_guidance as gg

    defects = [DEFECT_CANON[d] for d in decision["defects"]]
    if not defects:
        return f"{gg.NORMAL_KO}\n\n{gg.NORMAL_EN}"

    battery = {"battery_id": result["battery_id"],
               "form": result["cell_type"], "defects": defects}
    try:
        kb, by_id = gg.load_kb()
        chunks = gg.retrieve(defects, kb, by_id)
        return gg.generate(battery, chunks)          # Claude API (ANTHROPIC_API_KEY ํ•„์š”)
    except Exception as e:
        # ํด๋ฐฑ: ๊ฒฐ์ •์  ๋งคํ•‘๋งŒ์œผ๋กœ ๊ทผ๊ฑฐ ์กฐํ•ญ ๋‚˜์—ด (API ๋ถˆ๊ฐ€ ์‹œ ๋ฐ๋ชจ ์ง€์†)
        try:
            kb, by_id = gg.load_kb()
            mapping = kb["meta"]["defect_to_chunk"]
            refs = []
            for d in defects:
                for cid in mapping.get(d, []):
                    c = by_id.get(cid)
                    if c:
                        refs.append(f"- [{c['source']} ยท {c['ref']}] {c['text_ko']}")
            body = "\n".join(dict.fromkeys(refs)) or "(๋งคํ•‘ ์กฐํ•ญ ์—†์Œ)"
            return (f"โš ๏ธ AI ์ง€์นจ ์ƒ์„ฑ ๋ถˆ๊ฐ€({e}) โ†’ ๊ทœ์ • ๋งคํ•‘๋งŒ ํ‘œ์‹œ.\n"
                    f"ํƒ์ง€ ๊ฒฐํ•จ: {', '.join(defects)}\n{body}")
        except Exception:
            return f"โš ๏ธ ์ง€์นจ ์ƒ์„ฑ ๋ถˆ๊ฐ€: {e}"