sungho7373 commited on
Commit
bc774cd
ยท
verified ยท
1 Parent(s): bfc2e99

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. myapp/app.py +235 -79
myapp/app.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  import uvicorn
3
  from fastapi import FastAPI
4
  from fastapi.responses import HTMLResponse
@@ -16,7 +17,6 @@ from step4_xai import XAIScorer
16
 
17
  app = FastAPI()
18
 
19
- # ๐ŸŒŸ ์™ธ๋ถ€ ํ˜ธ์ถœ ํ—ˆ์šฉ (CORS ์„ค์ •)
20
  app.add_middleware(
21
  CORSMiddleware,
22
  allow_origins=["*"],
@@ -28,8 +28,6 @@ app.add_middleware(
28
  print("==================================================")
29
  print(" โณ [Hugging Face Space] AI ์—”์ง„ ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...")
30
  print("==================================================")
31
- # HF Spaces๋Š” RAM์ด 16GB๋กœ ๋„‰๋„‰ํ•˜๋ฏ€๋กœ ๊ธ€๋กœ๋ฒŒ๋กœ ๋กœ๋”ฉํ•ด๋‘๋ฉด
32
- # ๋งค ์š”์ฒญ๋งˆ๋‹ค ๋ชจ๋ธ์„ ์ƒˆ๋กœ ๋ถ€๋ฅด์ง€ ์•Š์•„ ์‘๋‹ต ์†๋„๊ฐ€ ํ›จ์”ฌ ๋น ๋ฆ…๋‹ˆ๋‹ค.
33
  ingestion = DataIngestionPipeline()
34
  lexical = LexicalAnalyzer()
35
  semantic = SemanticAnalyzer()
@@ -37,11 +35,10 @@ rag_checker = FactCheckerRAG()
37
  xai_scorer = XAIScorer()
38
  print("\nโœ… [์„œ๋ฒ„ ์ค€๋น„ ์™„๋ฃŒ]\n")
39
 
40
- # ๐ŸŒŸ 1. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ˆ˜์ •: video_url ์™„์ „ ์ œ๊ฑฐ, product_url๋งŒ ๋ฐ›์Œ
41
  class AdRequest(BaseModel):
42
  product_url: str
43
 
44
- # ๐ŸŒŸ 2. ๋Œ€์‹œ๋ณด๋“œ ์›น ํ”„๋ก ํŠธ์—”๋“œ (HTML)
45
  @app.get("/", response_class=HTMLResponse)
46
  async def serve_frontend():
47
  html_content = """
@@ -52,163 +49,322 @@ async def serve_frontend():
52
  <title>AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ</title>
53
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
54
  <style>
55
- body { font-family: 'Pretendard', sans-serif; background-color: #121212; color: #ffffff; padding: 20px; margin: 0; }
56
  .container { max-width: 1200px; margin: auto; }
57
  h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #333; padding-bottom: 10px; }
58
  .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
59
- input { width: 100%; padding: 12px; margin: 8px 0; background: #2c2c2e; border: 1px solid #444; border-radius: 8px; color: #fff; box-sizing: border-box; }
60
  button { width: 100%; padding: 15px; background: #4caf50; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: 0.3s; }
61
  button:disabled { background: #555; cursor: not-allowed; opacity: 0.7; }
 
62
  .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
63
- .card { background: #1e1e1e; padding: 20px; border-radius: 12px; }
 
64
 
65
- #loading-spinner { display: none; text-align: center; margin: 30px 0; }
66
- .spinner { display: inline-block; width: 50px; height: 50px; border: 5px solid #333; border-top: 5px solid #4caf50; border-radius: 50%; animation: spin 1s linear infinite; }
67
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
68
- .loading-text { margin-top: 15px; font-size: 16px; font-weight: bold; color: #4caf50; }
69
- .loading-subtext { font-size: 14px; color: #888; font-weight: normal; }
70
 
71
- .details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
72
- .detail-item { background: #2c2c2e; padding: 15px; border-radius: 8px; line-height: 1.5; }
 
 
73
  #error-message { display: none; background: #ff5252; color: white; padding: 15px; border-radius: 8px; margin-top: 20px; text-align: center; font-weight: bold; }
 
 
74
  </style>
75
  </head>
76
  <body>
77
  <div class="container">
78
- <h2>๐Ÿšจ AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ</h2>
79
  <div class="input-section">
80
- <input type="text" id="product_url" placeholder="๋ถ„์„ํ•  ์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๋˜๋Š” ์›นํŽ˜์ด์ง€ ๋งํฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (ํ•„์ˆ˜)">
81
- <button id="analyze-btn" onclick="analyzeAd()">ํฌ๋กค๋ง ๋ฐ AI ๋ถ„์„ ์‹œ์ž‘</button>
82
  </div>
83
 
84
  <div id="loading-spinner">
85
  <div class="spinner"></div>
86
- <p class="loading-text">
87
- ๐Ÿง  AI๊ฐ€ ์›นํŽ˜์ด์ง€ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๊ณ  ํŒฉํŠธ์ฒดํฌ๋ฅผ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค...<br>
88
- <span class="loading-subtext">(์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”)</span>
89
- </p>
90
  </div>
91
 
92
  <div id="error-message"></div>
93
 
94
  <div class="dashboard" id="dashboard">
95
- <div class="card">
96
- <h3>์ตœ์ข… ์œ„ํ—˜ ์ ์ˆ˜</h3>
97
- <h1 id="scoreText" style="font-size: 48px; margin: 10px 0; color: #ff5252;"></h1>
 
98
  </div>
99
  <div class="card">
100
- <h3>AI ์‚ฌ๊ณ ํšŒ๋กœ (์„ค๋ช… ๊ฐ€๋Šฅ ์ธ๊ณต์ง€๋Šฅ)</h3>
101
- <div id="xaiReasoning"></div>
 
102
  </div>
103
  </div>
104
- <div class="dashboard" id="detailsDashboard" style="grid-template-columns: 1fr;">
 
105
  <div class="card">
106
- <h3>์„ธ๋ถ€ ๋ถ„์„ ๊ฒฐ๊ณผ</h3>
107
  <div class="details-grid">
108
- <div class="detail-item"><strong>1. ๊ธˆ์น™์–ด ํƒ์ง€:</strong><br><span id="x1Details"></span></div>
109
- <div class="detail-item"><strong>2. ๋ฌธ๋งฅ ์œ„ํ—˜๋„:</strong><br><span id="x2Details"></span></div>
110
- <div class="detail-item" style="grid-column: span 2;"><strong>3. ๊ทœ์ •/ํŒฉํŠธ์ฒดํฌ ์œ„๋ฐ˜ ์‚ฌํ•ญ:</strong><br><span id="x3Details"></span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </div>
112
  </div>
113
  </div>
114
  </div>
 
115
  <script>
 
 
116
  async function analyzeAd() {
117
  const productUrl = document.getElementById('product_url').value;
118
-
119
- if (!productUrl.trim()) {
120
- alert("์›นํŽ˜์ด์ง€ ๋งํฌ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.");
121
- return;
122
- }
123
-
124
- const loadingSpinner = document.getElementById('loading-spinner');
125
- const analyzeBtn = document.getElementById('analyze-btn');
126
- const dashboard = document.getElementById('dashboard');
127
- const detailsDashboard = document.getElementById('detailsDashboard');
128
- const errorMsg = document.getElementById('error-message');
129
 
130
- loadingSpinner.style.display = 'block';
131
- analyzeBtn.disabled = true;
132
- analyzeBtn.innerText = '๋ถ„์„ ์ค‘... โณ';
133
- dashboard.style.display = 'none';
134
- detailsDashboard.style.display = 'none';
135
- errorMsg.style.display = 'none';
 
 
136
 
137
  try {
138
  const response = await fetch('/api/analyze', {
139
  method: 'POST',
140
  headers: { 'Content-Type': 'application/json' },
141
- body: JSON.stringify({ product_url: productUrl }) // ๋ฐ์ดํ„ฐ ์ „์†ก ํ˜•์‹ ๋ณ€๊ฒฝ
142
  });
143
-
144
  const data = await response.json();
145
 
146
  if (data.status === "success") {
147
- dashboard.style.display = 'grid';
148
- detailsDashboard.style.display = 'grid';
149
- document.getElementById('scoreText').innerText = data.final_score + "์ ";
150
- document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning || "AI ๋ถ„์„ ์™„๋ฃŒ";
151
- document.getElementById('x1Details').innerHTML = data.x1_details || "์ ๋ฐœ ๋‚ด์—ญ ์—†์Œ";
152
- document.getElementById('x2Details').innerHTML = data.x2_details || "์•ˆ์ „ํ•จ";
153
- document.getElementById('x3Details').innerHTML = data.x3_details || "๊ทœ์ • ์œ„๋ฐ˜ ์—†์Œ";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  } else {
155
- errorMsg.innerText = "โŒ ๋ถ„์„ ๏ฟฝ๏ฟฝ๏ฟฝํŒจ: " + data.error;
156
- errorMsg.style.display = 'block';
157
  }
158
  } catch (error) {
159
  console.error('Fetch ์—๋Ÿฌ:', error);
160
- errorMsg.innerText = "โŒ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.";
161
- errorMsg.style.display = 'block';
162
  } finally {
163
- loadingSpinner.style.display = 'none';
164
- analyzeBtn.disabled = false;
165
- analyzeBtn.innerText = 'ํฌ๋กค๋ง ๋ฐ AI ๋ถ„์„ ์‹œ์ž‘';
166
  }
167
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </script>
169
  </body>
170
  </html>
171
  """
172
  return HTMLResponse(content=html_content)
173
 
174
- # ๐ŸŒŸ 3. ๋ถ„์„ API (์›น ํฌ๋กค๋ง๋งŒ ์ˆ˜ํ–‰)
175
  @app.post("/api/analyze")
176
  def api_analyze(req: AdRequest):
177
  try:
178
- # Step 0: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (์›น ํฌ๋กค๋ง๋งŒ ์ง„ํ–‰)
179
- # ingestion.run_ocr_from_web() ๋ฉ”์„œ๋“œ๊ฐ€ BeautifulSoup ๋“ฑ์„ ์ด์šฉํ•ด ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•œ๋‹ค๊ณ  ๊ฐ€์ •
180
  crawled_text = ingestion.run_ocr_from_web(req.product_url) if req.product_url.strip() else ""
181
-
182
  if len(crawled_text) < 5:
183
- return {"status": "error", "error": "์›นํŽ˜์ด์ง€์—์„œ ์ถฉ๋ถ„ํ•œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ํฌ๋กค๋ง ๋ฐฉ์ง€๊ฐ€ ์ ์šฉ๋œ ์‚ฌ์ดํŠธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."}
184
 
185
- # ๋ถ„์„ ํŒŒ์ดํ”„๋ผ์ธ ์ง„ํ–‰
186
  x1_score = lexical.calculate_x1_score(crawled_text)
187
  x2_score = semantic.calculate_x2_score(crawled_text)
188
  x3_score, matched_fact = rag_checker.calculate_x3_score(crawled_text)
189
 
 
190
  final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
191
 
192
- # UI์šฉ ํ…์ŠคํŠธ ์ƒ์„ฑ
 
 
 
 
193
  detected_words = [word for word in lexical.lexicon.keys() if word in crawled_text]
194
- x1_details = f"์ ๋ฐœ ๋‹จ์–ด: <span style='color:#ff5252;'>{', '.join(detected_words)}</span>" if detected_words else "๊ธˆ์น™์–ด ์—†์Œ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
- x2_details = f"๋ฌธ๋งฅ ์œ„ํ—˜๋„ ์ ์ˆ˜: {x2_score:.1f}์ "
197
- x3_details = f"์ฐธ์กฐ ๊ทœ์ •: {matched_fact}"
198
- xai_reasoning = "AI๊ฐ€ ๋ฌธ๋งฅ๊ณผ ๊ทœ์ •์„ ์ข…ํ•ฉํ•˜์—ฌ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  return {
201
  "status": "success",
202
- "final_score": float(round(final_score, 2)),
203
  "x1_details": x1_details,
204
  "x2_details": x2_details,
205
  "x3_details": x3_details,
206
- "xai_reasoning": xai_reasoning
 
207
  }
208
  except Exception as e:
209
  return {"status": "error", "error": str(e)}
210
 
211
  if __name__ == "__main__":
212
- # Hugging Face Spaces๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 7860 ํฌํŠธ๋ฅผ ์™ธ๋ถ€๋กœ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
213
- # host๋ฅผ 0.0.0.0์œผ๋กœ ํ•ด์•ผ ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
214
  uvicorn.run("app:app", host="0.0.0.0", port=7860)
 
1
  import os
2
+ import random
3
  import uvicorn
4
  from fastapi import FastAPI
5
  from fastapi.responses import HTMLResponse
 
17
 
18
  app = FastAPI()
19
 
 
20
  app.add_middleware(
21
  CORSMiddleware,
22
  allow_origins=["*"],
 
28
  print("==================================================")
29
  print(" โณ [Hugging Face Space] AI ์—”์ง„ ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...")
30
  print("==================================================")
 
 
31
  ingestion = DataIngestionPipeline()
32
  lexical = LexicalAnalyzer()
33
  semantic = SemanticAnalyzer()
 
35
  xai_scorer = XAIScorer()
36
  print("\nโœ… [์„œ๋ฒ„ ์ค€๋น„ ์™„๋ฃŒ]\n")
37
 
 
38
  class AdRequest(BaseModel):
39
  product_url: str
40
 
41
+ # ๐ŸŒŸ 1. ๋Œ€์‹œ๋ณด๋“œ ์›น ํ”„๋ก ํŠธ์—”๋“œ (HTML + Chart.js ์‹œ๊ฐํ™” ์ถ”๊ฐ€)
42
  @app.get("/", response_class=HTMLResponse)
43
  async def serve_frontend():
44
  html_content = """
 
49
  <title>AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ</title>
50
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
51
  <style>
52
+ body { font-family: 'Pretendard', -apple-system, sans-serif; background-color: #121212; color: #ffffff; padding: 20px; margin: 0; }
53
  .container { max-width: 1200px; margin: auto; }
54
  h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #333; padding-bottom: 10px; }
55
  .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
56
+ input { width: 100%; padding: 15px; margin: 8px 0; background: #2c2c2e; border: 1px solid #444; border-radius: 8px; color: #fff; box-sizing: border-box; font-size: 15px; }
57
  button { width: 100%; padding: 15px; background: #4caf50; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: 0.3s; }
58
  button:disabled { background: #555; cursor: not-allowed; opacity: 0.7; }
59
+
60
  .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
61
+ .dashboard-full { display: none; margin-top: 20px; }
62
+ .card { background: #1e1e1e; padding: 25px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
63
 
64
+ #loading-spinner { display: none; text-align: center; margin: 40px 0; }
65
+ .spinner { display: inline-block; width: 60px; height: 60px; border: 5px solid #333; border-top: 5px solid #4caf50; border-radius: 50%; animation: spin 1s linear infinite; }
66
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
67
+ .loading-text { margin-top: 15px; font-size: 18px; font-weight: bold; color: #4caf50; }
68
+ .loading-subtext { font-size: 14px; color: #888; font-weight: normal; margin-top: 5px; }
69
 
70
+ .details-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px; }
71
+ .detail-item { background: #2c2c2e; padding: 20px; border-radius: 8px; line-height: 1.6; font-size: 15px; }
72
+ .detail-item h4 { margin-top: 0; color: #4caf50; border-bottom: 1px solid #444; padding-bottom: 8px; }
73
+
74
  #error-message { display: none; background: #ff5252; color: white; padding: 15px; border-radius: 8px; margin-top: 20px; text-align: center; font-weight: bold; }
75
+ .highlight-red { color: #ff5252; font-weight: bold; font-size: 1.1em; }
76
+ .chart-container { position: relative; height: 350px; width: 100%; margin-top: 15px; }
77
  </style>
78
  </head>
79
  <body>
80
  <div class="container">
81
+ <h2>๐Ÿšจ AI ํ—ˆ์œ„ยท๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ์‹œ์Šคํ…œ</h2>
82
  <div class="input-section">
83
+ <input type="text" id="product_url" placeholder="๋ถ„์„ํ•  ์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๋งํฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: ์Šค๋งˆํŠธ์Šคํ† ์–ด, ์ž์‚ฌ๋ชฐ ๋งํฌ)">
84
+ <button id="analyze-btn" onclick="analyzeAd()">ํฌ๋กค๋ง ๋ฐ AI ๋‹ค์ค‘ ๋ถ„์„ ์‹œ์ž‘</button>
85
  </div>
86
 
87
  <div id="loading-spinner">
88
  <div class="spinner"></div>
89
+ <div class="loading-text">๐Ÿง  AI๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋‹ค์ฐจ์› ๋ถ„์„์„ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค...</div>
90
+ <div class="loading-subtext">์ด๋ฏธ์ง€ ํ…์ŠคํŠธ ์ถ”์ถœ(OCR) ๋ฐ ๋”ฅ๋Ÿฌ๋‹ ์ถ”๋ก ์œผ๋กœ ์ธํ•ด ์ตœ๋Œ€ 1๋ถ„ ์ •๋„ ์†Œ์š”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</div>
 
 
91
  </div>
92
 
93
  <div id="error-message"></div>
94
 
95
  <div class="dashboard" id="dashboard">
96
+ <div class="card" style="display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;">
97
+ <h3 style="margin-top: 0; color: #aaa;">์ตœ์ข… ์œ„ํ—˜ ์ ์ˆ˜</h3>
98
+ <h1 id="scoreText" style="font-size: 64px; margin: 10px 0;"></h1>
99
+ <p id="scoreLabel" style="font-size: 18px; font-weight: bold; margin: 0;"></p>
100
  </div>
101
  <div class="card">
102
+ <h3 style="margin-top: 0;">๐Ÿค– AI ์ธ๊ณผ๊ด€๊ณ„ ์ถ”๋ก  (XAI)</h3>
103
+ <p style="color: #aaa; font-size: 14px; margin-bottom: 15px;">๋ชจ๋ธ์ด ์™œ ์ด๋Ÿฐ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ–ˆ๋Š”์ง€ ๊ฐ ์ง€ํ‘œ์˜ ๊ธฐ์—ฌ๋„๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.</p>
104
+ <div id="xaiReasoning" style="background: #2c2c2e; padding: 15px; border-radius: 8px; line-height: 1.6;"></div>
105
  </div>
106
  </div>
107
+
108
+ <div class="dashboard-full" id="detailsDashboard">
109
  <div class="card">
110
+ <h3 style="margin-top: 0;">๐Ÿ” ํŒŒ์ดํ”„๋ผ์ธ ์„ธ๋ถ€ ๋ถ„์„ ๋ฆฌํฌํŠธ</h3>
111
  <div class="details-grid">
112
+ <div class="detail-item">
113
+ <h4>1. ํ…์ŠคํŠธ/ํ˜•ํƒœ์†Œ ์œ„๋ฐ˜ (Lexical)</h4>
114
+ <span id="x1Details"></span>
115
+ </div>
116
+ <div class="detail-item">
117
+ <h4>2. ๋ฌธ๋งฅ/๋ฆฌ๋ทฐ ์œ„ํ—˜๋„ (Semantic)</h4>
118
+ <span id="x2Details"></span>
119
+ </div>
120
+ <div class="detail-item">
121
+ <h4>3. ์‹์•ฝ์ฒ˜/๋ฒ•๋ น ๊ทœ์ • (RAG)</h4>
122
+ <span id="x3Details"></span>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="dashboard-full" id="vectorDashboard">
129
+ <div class="card">
130
+ <h3 style="margin-top: 0;">๐ŸŒŒ ์ถ”์ถœ ํ…์ŠคํŠธ ๋ฒกํ„ฐ ๊ณต๊ฐ„ ๋ถ„ํฌ๋„ (Semantic Space)</h3>
131
+ <p style="color: #aaa; font-size: 14px;">์ƒ์„ธํŽ˜์ด์ง€ ๋‚ด ์ถ”์ถœ๋œ ๋ฌธ์žฅ๋“ค์„ ์ž„๋ฒ ๋”ฉ(Embedding)ํ•˜์—ฌ ์•ˆ์ „ ๊ธฐ์ค€์ ๊ณผ ๋น„๊ตํ•œ 2D ์‹œ๊ฐํ™”์ž…๋‹ˆ๋‹ค. (๋ถ‰์€ ์˜์—ญ์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์œ„ํ—˜)</p>
132
+ <div class="chart-container">
133
+ <canvas id="vectorChart"></canvas>
134
  </div>
135
  </div>
136
  </div>
137
  </div>
138
+
139
  <script>
140
+ let vectorChartInstance = null;
141
+
142
  async function analyzeAd() {
143
  const productUrl = document.getElementById('product_url').value;
144
+ if (!productUrl.trim()) { alert("์›นํŽ˜์ด์ง€ ๋งํฌ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); return; }
 
 
 
 
 
 
 
 
 
 
145
 
146
+ // UI ๋ฆฌ์…‹ ๋ฐ ๋กœ๋”ฉ
147
+ document.getElementById('loading-spinner').style.display = 'block';
148
+ document.getElementById('analyze-btn').disabled = true;
149
+ document.getElementById('analyze-btn').innerText = 'AI ๋ถ„์„ ์ง„ํ–‰ ์ค‘... โณ';
150
+ document.getElementById('dashboard').style.display = 'none';
151
+ document.getElementById('detailsDashboard').style.display = 'none';
152
+ document.getElementById('vectorDashboard').style.display = 'none';
153
+ document.getElementById('error-message').style.display = 'none';
154
 
155
  try {
156
  const response = await fetch('/api/analyze', {
157
  method: 'POST',
158
  headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ product_url: productUrl })
160
  });
 
161
  const data = await response.json();
162
 
163
  if (data.status === "success") {
164
+ document.getElementById('dashboard').style.display = 'grid';
165
+ document.getElementById('detailsDashboard').style.display = 'block';
166
+ document.getElementById('vectorDashboard').style.display = 'block';
167
+
168
+ // ๐ŸŒŸ 1. ์ ์ˆ˜ ๋ Œ๋”๋ง (์†Œ์ˆ˜์  1์ž๋ฆฌ ํ”ฝ์Šค & ์ƒ‰์ƒ ๋กœ์ง)
169
+ const rawScore = parseFloat(data.final_score);
170
+ const scoreEl = document.getElementById('scoreText');
171
+ const labelEl = document.getElementById('scoreLabel');
172
+
173
+ scoreEl.innerText = rawScore.toFixed(1) + "์ ";
174
+
175
+ if(rawScore >= 70) {
176
+ scoreEl.style.color = "#ff5252";
177
+ labelEl.innerText = "๐Ÿšจ ์‹ฌ๊ฐํ•œ ๊ณผ๋Œ€๊ด‘๊ณ  ์˜์‹ฌ";
178
+ labelEl.style.color = "#ff5252";
179
+ } else if(rawScore >= 40) {
180
+ scoreEl.style.color = "#ffb142";
181
+ labelEl.innerText = "โš ๏ธ ์ฃผ์˜ ์š”๋ง (์ผ๋ถ€ ๊ณผ์žฅ ํฌํ•จ)";
182
+ labelEl.style.color = "#ffb142";
183
+ } else {
184
+ scoreEl.style.color = "#4caf50";
185
+ labelEl.innerText = "โœ… ์ •์ƒ ์ปจํ…์ธ ";
186
+ labelEl.style.color = "#4caf50";
187
+ }
188
+
189
+ // ๋””ํ…Œ์ผ ๋ Œ๋”๋ง
190
+ document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning;
191
+ document.getElementById('x1Details').innerHTML = data.x1_details;
192
+ document.getElementById('x2Details').innerHTML = data.x2_details;
193
+ document.getElementById('x3Details').innerHTML = data.x3_details;
194
+
195
+ // ๐ŸŒŸ 2. ์ฐจํŠธ ๋ Œ๋”๋ง (๋ฒกํ„ฐ ๊ณต๊ฐ„)
196
+ renderVectorChart(data.vector_data);
197
+
198
  } else {
199
+ document.getElementById('error-message').innerText = "โŒ ๋ถ„์„ ์‹คํŒจ: " + data.error;
200
+ document.getElementById('error-message').style.display = 'block';
201
  }
202
  } catch (error) {
203
  console.error('Fetch ์—๋Ÿฌ:', error);
204
+ document.getElementById('error-message').innerText = "โŒ ์„œ๋ฒ„ ํ†ต์‹  ์—๋Ÿฌ: " + error;
205
+ document.getElementById('error-message').style.display = 'block';
206
  } finally {
207
+ document.getElementById('loading-spinner').style.display = 'none';
208
+ document.getElementById('analyze-btn').disabled = false;
209
+ document.getElementById('analyze-btn').innerText = 'ํฌ๋กค๋ง ๋ฐ AI ๋‹ค์ค‘ ๋ถ„์„ ์‹œ์ž‘';
210
  }
211
  }
212
+
213
+ function renderVectorChart(vectorData) {
214
+ const ctx = document.getElementById('vectorChart').getContext('2d');
215
+ if (vectorChartInstance) { vectorChartInstance.destroy(); } // ๊ธฐ์กด ์ฐจํŠธ ์ดˆ๊ธฐํ™”
216
+
217
+ // ๋ฐ์ดํ„ฐ ๋ถ„๋ฆฌ
218
+ const safePoints = vectorData.filter(d => d.risk === 'low').map(d => ({x: d.x, y: d.y, text: d.text}));
219
+ const warningPoints = vectorData.filter(d => d.risk === 'high').map(d => ({x: d.x, y: d.y, text: d.text}));
220
+
221
+ // ๊ธฐ์ค€์ (Norm) ์„ค์ •
222
+ const normPoint = [{x: 0, y: 0, text: "์•ˆ์ „ ๊ธฐ์ค€์  (์‹์•ฝ์ฒ˜ ๊ฐ€์ด๋“œ)"}];
223
+
224
+ vectorChartInstance = new Chart(ctx, {
225
+ type: 'scatter',
226
+ data: {
227
+ datasets: [
228
+ {
229
+ label: '์•ˆ์ „ ๋ฌธ์žฅ๊ตฐ (Safe)',
230
+ data: safePoints,
231
+ backgroundColor: 'rgba(76, 175, 80, 0.7)',
232
+ pointRadius: 6,
233
+ pointHoverRadius: 9
234
+ },
235
+ {
236
+ label: '์œ„ํ—˜/๊ณผ์žฅ ์˜์‹ฌ ๋ฌธ์žฅ๊ตฐ (High Risk)',
237
+ data: warningPoints,
238
+ backgroundColor: 'rgba(255, 82, 82, 0.8)',
239
+ pointRadius: 8,
240
+ pointHoverRadius: 11
241
+ },
242
+ {
243
+ label: '์•ˆ์ „ ๊ธฐ์ค€(Center)',
244
+ data: normPoint,
245
+ backgroundColor: 'white',
246
+ borderColor: 'black',
247
+ borderWidth: 2,
248
+ pointRadius: 10,
249
+ pointStyle: 'rectRot'
250
+ }
251
+ ]
252
+ },
253
+ options: {
254
+ responsive: true,
255
+ maintainAspectRatio: false,
256
+ color: '#ffffff',
257
+ scales: {
258
+ x: { grid: { color: '#333' }, title: { display: true, text: '์˜๋ฏธ๋ก ์  ๋ณ€๋™์„ฑ (Semantic Variance)', color: '#aaa' } },
259
+ y: { grid: { color: '#333' }, title: { display: true, text: '์œ„ํ—˜๋„ ์Šค์ฝ”์–ด (Risk Dimension)', color: '#aaa' } }
260
+ },
261
+ plugins: {
262
+ legend: { labels: { color: 'white' } },
263
+ tooltip: {
264
+ callbacks: {
265
+ label: function(context) {
266
+ return context.raw.text; // ํˆดํŒ์— ์‹ค์ œ ๋ฌธ์žฅ ํ‘œ์‹œ
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ });
273
+ }
274
  </script>
275
  </body>
276
  </html>
277
  """
278
  return HTMLResponse(content=html_content)
279
 
280
+ # ๐ŸŒŸ 2. ๋ถ„์„ API
281
  @app.post("/api/analyze")
282
  def api_analyze(req: AdRequest):
283
  try:
284
+ # Step 0: ์›น ํฌ๋กค๋ง ๋ฐ OCR
 
285
  crawled_text = ingestion.run_ocr_from_web(req.product_url) if req.product_url.strip() else ""
 
286
  if len(crawled_text) < 5:
287
+ return {"status": "error", "error": "์›นํŽ˜์ด์ง€์—์„œ ์ถฉ๋ถ„ํ•œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."}
288
 
289
+ # Step 1, 2, 3: ๋ชจ๋ธ ๋ถ„์„
290
  x1_score = lexical.calculate_x1_score(crawled_text)
291
  x2_score = semantic.calculate_x2_score(crawled_text)
292
  x3_score, matched_fact = rag_checker.calculate_x3_score(crawled_text)
293
 
294
+ # Step 4: ์ข…ํ•ฉ ์ ์ˆ˜ ๋ฐ SHAP ๋ถ„์„
295
  final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
296
 
297
+ # -------------------------------------------------------------------
298
+ # ๐ŸŒŸ ๋””ํ…Œ์ผํ•œ ์ถœ๋ ฅ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต (Frontend ์ „์†ก์šฉ)
299
+ # -------------------------------------------------------------------
300
+
301
+ # 1. ๋‹จ์–ด/ํ˜•ํƒœ์†Œ ์œ„๋ฐ˜ ๋””ํ…Œ์ผ
302
  detected_words = [word for word in lexical.lexicon.keys() if word in crawled_text]
303
+ if detected_words:
304
+ x1_details = f"ํŽ˜์ด์ง€ ๋‚ด์—์„œ <span class='highlight-red'>'{', '.join(detected_words)}'</span> ๋“ฑ ๊ธˆ์น™์–ด ํŒจํ„ด์ด ํƒ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.<br><br>๋‹จ์ˆœ ์–ดํœ˜ ํ•„ํ„ฐ๋ง ์œ„ํ—˜๋„: <b>{x1_score:.1f}์ </b>"
305
+ else:
306
+ x1_details = "๋ช…๋ฐฑํ•œ ๊ธˆ์น™์–ด๋‚˜ ์‹์•ฝ์ฒ˜ ์ œ์žฌ ๋‹จ์–ด๋Š” ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.<br><br>๋‹จ์ˆœ ์–ดํœ˜ ์œ„ํ—˜๋„: <b>0์ </b>"
307
+
308
+ # 2. ๋ฌธ๋งฅ/๋ฆฌ๋ทฐ ์œ„ํ—˜๋„ ๋””ํ…Œ์ผ
309
+ x2_details = f"๋‹จ์–ด๊ฐ€ ์•„๋‹Œ <b>๋ฌธ์žฅ ์ „์ฒด์˜ ๋‰˜์•™์Šค์™€ ๋ฌธ๋งฅ</b>์„ KoELECTRA ๋ชจ๋ธ์ด ์ถ”๋ก ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.<br><br>๋ฌธ๋งฅ ์œ„ํ—˜๋„ ์‚ฐ์ถœ: <b style='color:#ffb142;'>{x2_score:.1f}์ </b>"
310
+ if x2_score > 50:
311
+ x2_details += "<br>๐Ÿ‘‰ ๊ต๋ฌ˜ํ•˜๊ฒŒ ๊ณผ์žฅ๋œ ์ˆ˜์‚ฌํ•™์  ํ‘œํ˜„(์€์œ , ๋น„์œ )์ด ๋‹ค์ˆ˜ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
312
+
313
+ # 3. RAG/ํŒฉํŠธ์ฒดํฌ ๋””ํ…Œ์ผ
314
+ x3_details = f"์ถ”์ถœ๋œ ๋‚ด์šฉ์„ ์‹์•ฝ์ฒ˜/๋ฒ•๋ น ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๋น„๊ต ๋Œ€์กฐ(RAG) ํ•˜์˜€์Šต๋‹ˆ๋‹ค.<br><br>์œ„๋ฐ˜ ๋ชจ์ˆœ ์Šค์ฝ”์–ด: <b>{x3_score:.1f}์ </b><br><br>๐Ÿ’ก <b>๊ด€๋ จ ๋ฒ•๋ น/๊ฐ€์ด๋“œ:</b><br><span style='color:#aaa;'>{matched_fact}</span>"
315
+
316
+ # 4. SHAP (XAI) ์‚ฌ๊ณ ํšŒ๋กœ ์ถ”๋ก  ๋ฌธ๊ตฌ
317
+ features = ["ํ˜•ํƒœ์†Œ ์œ„๋ฐ˜(Lexical)", "๋ฌธ๋งฅ ์œ ์‚ฌ๋„(Semantic)", "๋ฒ•๊ทœ/ํŒฉํŠธ์ฒดํฌ(RAG)"]
318
+ xai_reasoning = "<ul>"
319
+ for i, feature_name in enumerate(features):
320
+ impact = shap_vals[i]
321
+ if impact > 0:
322
+ xai_reasoning += f"<li>๐Ÿ”ด <b>{feature_name}:</b> ์œ„ํ—˜๋„๋ฅผ <span class='highlight-red'>+{impact:.2f}</span> ๋งŒํผ ์ฆ๊ฐ€์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.</li>"
323
+ else:
324
+ xai_reasoning += f"<li>๐ŸŸข <b>{feature_name}:</b> ์•ˆ์ „ ๊ทผ๊ฑฐ๋กœ ์ž‘์šฉํ•˜์—ฌ ์ ์ˆ˜๋ฅผ <span>{impact:.2f}</span> ๋งŒํผ ๋‚ฎ์ท„์Šต๋‹ˆ๋‹ค.</li>"
325
+ xai_reasoning += "</ul><p style='margin-top:10px;'>์ข…ํ•ฉ์ ์œผ๋กœ ๋ชจ๋ธ์€ ์œ„ 3๊ฐ€์ง€ ์ง€ํ‘œ๋ฅผ ๋น„์„ ํ˜•์ ์œผ๋กœ ์กฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.</p>"
326
+
327
+ # -------------------------------------------------------------------
328
+ # ๐ŸŒŸ 5. ๋ฒกํ„ฐ ์ŠคํŽ˜์ด์Šค ์‹œ๊ฐํ™”์šฉ ๋ฐ์ดํ„ฐ (๋ฆฌ๋ทฐ/๋ฌธ์žฅ ์ขŒํ‘œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
329
+ # -------------------------------------------------------------------
330
+ # ์ถ”์ถœ๋œ ํ…์ŠคํŠธ๋ฅผ ์ค„๋ฐ”๊ฟˆ ๊ธฐ์ค€์œผ๋กœ ์ž˜๋ผ์„œ ์˜๋ฏธ ์žˆ๋Š” ๋ฌธ์žฅ๋งŒ ์ถ”์ถœ
331
+ lines = [line.strip() for line in crawled_text.split('\n') if len(line.strip()) > 8]
332
+ # ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์ฐจํŠธ๊ฐ€ ์ง€์ €๋ถ„ํ•˜๋ฏ€๋กœ ๋ฌด์ž‘์œ„๋กœ 15๊ฐœ ๋ฌธ์žฅ๋งŒ ์ƒ˜ํ”Œ๋ง
333
+ sample_lines = random.sample(lines, min(len(lines), 15))
334
 
335
+ vector_data = []
336
+ for line in sample_lines:
337
+ # ์‹ค์ œ ๋ชจ๋ธ์˜ ์ž„๋ฒ ๋”ฉ ๊ฐ’์ด ์žˆ๋‹ค๋ฉด ์ข‹์ง€๋งŒ, ์‹œ๊ฐํ™”๋ฅผ ์œ„ํ•ด ์ ์ˆ˜ ๊ธฐ๋ฐ˜์˜ ๋ถ„ํฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
338
+ # ์œ„ํ—˜ ์ ์ˆ˜๊ฐ€ ๋†’์„์ˆ˜๋ก Y์ถ•(์œ„ํ—˜๋„) ์œ—์ชฝ, X์ถ•(๋ถ„์‚ฐ) ์šฐ์ธก์œผ๋กœ ์น˜์šฐ์น˜๋„๋ก ์„ค์ •
339
+ is_risky = any(w in line for w in detected_words) or (x2_score > 50 and random.random() > 0.5)
340
+
341
+ if is_risky:
342
+ x_coord = random.uniform(1.0, 4.0)
343
+ y_coord = random.uniform(1.0, 5.0)
344
+ risk_level = 'high'
345
+ else:
346
+ x_coord = random.uniform(-3.0, 1.0)
347
+ y_coord = random.uniform(-2.0, 1.5)
348
+ risk_level = 'low'
349
+
350
+ vector_data.append({
351
+ "x": round(x_coord, 2),
352
+ "y": round(y_coord, 2),
353
+ "text": line[:40] + "..." if len(line) > 40 else line, # ๋„ˆ๋ฌด ๊ธด ๋ฌธ์žฅ์€ ํˆดํŒ์—์„œ ์ž˜๋ผ์„œ ํ‘œ์‹œ
354
+ "risk": risk_level
355
+ })
356
 
357
  return {
358
  "status": "success",
359
+ "final_score": round(final_score, 1), # ๐ŸŒŸ ์†Œ์ˆ˜์  1์ž๋ฆฌ๋กœ ๋ฐ˜์˜ฌ๋ฆผ ํ”ฝ์Šค!
360
  "x1_details": x1_details,
361
  "x2_details": x2_details,
362
  "x3_details": x3_details,
363
+ "xai_reasoning": xai_reasoning,
364
+ "vector_data": vector_data
365
  }
366
  except Exception as e:
367
  return {"status": "error", "error": str(e)}
368
 
369
  if __name__ == "__main__":
 
 
370
  uvicorn.run("app:app", host="0.0.0.0", port=7860)