sungho7373 commited on
Commit
6484389
ยท
1 Parent(s): 699791e
Files changed (1) hide show
  1. myapp/app.py +55 -242
myapp/app.py CHANGED
@@ -1,45 +1,16 @@
1
- from fastapi import FastAPI
2
- from fastapi.responses import HTMLResponse
3
- from fastapi.middleware.cors import CORSMiddleware # ๐ŸŒŸ CORS ์ถ”๊ฐ€
4
- from pydantic import BaseModel
5
- import uvicorn
6
  import math
7
-
8
- # (๊ธฐ์กด AI ๋ชจ๋“ˆ import ๋ถ€๋ถ„ ๋™์ผ)
9
-
10
- app = FastAPI()
11
-
12
- # ๐ŸŒŸ ์™ธ๋ถ€(Wix)์—์„œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก CORS ํ—ˆ์šฉ ์„ค์ •
13
- app.add_middleware(
14
- CORSMiddleware,
15
- allow_origins=["*"], # ์‹ค์ œ ์„œ๋น„์Šค ์‹œ์—๋Š” ["https://๋‚ดwix์ฃผ์†Œ.com"] ์œผ๋กœ ๋ณ€๊ฒฝ ๊ถŒ์žฅ
16
- allow_credentials=True,
17
- allow_methods=["*"],
18
- allow_headers=["*"],
19
- )
20
-
21
- # (์ดํ•˜ ๊ธฐ์กด ์ฝ”๋“œ ๋™์ผ...)
22
-
23
- from flask import Flask
24
-
25
- app = Flask(__name__)
26
-
27
- @app.route("/")
28
- def home():
29
- return "Hello from Render!"
30
-
31
- if __name__ == "__main__":
32
- app.run()
33
-
34
  from fastapi import FastAPI
35
  from fastapi.responses import HTMLResponse
 
36
  from pydantic import BaseModel
37
- import uvicorn
38
- import math
39
 
40
  # ==========================================
41
  # ๐ŸŒŸ AI ๋ ˆ๊ณ  ๋ธ”๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
42
  # ==========================================
 
 
43
  from step0_ingestion import DataIngestionPipeline
44
  from step1_lexical import LexicalAnalyzer
45
  from step2_semantic import SemanticAnalyzer
@@ -48,23 +19,35 @@ from step4_xai import XAIScorer
48
 
49
  app = FastAPI()
50
 
 
 
 
 
 
 
 
 
 
51
  print("==================================================")
52
  print(" โณ AI ์—”์ง„ ๋ฐ ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ๋“ค์„ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฌ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...")
53
  print("==================================================")
 
 
54
  ingestion = DataIngestionPipeline()
55
  lexical = LexicalAnalyzer()
56
  semantic = SemanticAnalyzer()
57
  rag_checker = FactCheckerRAG()
58
  xai_scorer = XAIScorer()
59
- print("\nโœ… [์„œ๋ฒ„ ์ค€๋น„ ์™„๋ฃŒ] http://127.0.0.1:8000 ์— ์ ‘์†ํ•˜์„ธ์š”!\n")
60
 
61
  class AdRequest(BaseModel):
62
  video_url: str
63
  product_url: str
64
 
65
- # 1. ๐ŸŒŸ ๋‹คํฌ ๋ชจ๋“œ ๋Œ€์‹œ๋ณด๋“œ ์›น ํ”„๋ก ํŠธ์—”๋“œ (HTML/CSS/JS)
66
  @app.get("/", response_class=HTMLResponse)
67
  async def serve_frontend():
 
68
  html_content = """
69
  <!DOCTYPE html>
70
  <html lang="ko">
@@ -76,195 +59,63 @@ async def serve_frontend():
76
  body { font-family: 'Pretendard', sans-serif; background-color: #121212; color: #ffffff; padding: 20px; margin: 0; }
77
  .container { max-width: 1200px; margin: auto; }
78
  h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #333; padding-bottom: 10px; }
79
-
80
- /* ์ž…๋ ฅ ํผ ์„น์…˜ */
81
  .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
82
  input { width: 100%; padding: 12px; margin: 8px 0; background: #2c2c2e; border: 1px solid #444; border-radius: 8px; color: #fff; box-sizing: border-box; }
83
  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; }
84
- button:hover { background: #45a049; }
85
-
86
- /* ๋Œ€์‹œ๋ณด๋“œ ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ */
87
  .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
88
- .card { background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
89
- .card h3 { margin-top: 0; color: #a0a0a0; font-size: 16px; border-bottom: 1px solid #333; padding-bottom: 10px; }
90
-
91
- /* ๋„๋„› ์ฐจํŠธ ์นด๋“œ */
92
- .score-card { text-align: center; }
93
- .score-card canvas { max-height: 250px; margin: auto; }
94
-
95
- /* ์‚ฌ๊ณ ํšŒ๋กœ ์นด๋“œ */
96
- .xai-card { font-size: 15px; line-height: 1.6; color: #e0e0e0; }
97
- .highlight { color: #4caf50; font-weight: bold; }
98
  .danger { color: #ff5252; font-weight: bold; }
99
-
100
- /* ์„ธ๋ถ€ ์—”์ง„ ๊ฒฐ๊ณผ ์นด๋“œ */
101
  .details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
102
  .detail-item { background: #2c2c2e; padding: 15px; border-radius: 8px; }
103
- .detail-item h4 { margin: 0 0 10px 0; color: #4caf50; }
104
-
105
- /* ๋กœ๋”ฉ ํ…์ŠคํŠธ */
106
- #loading { display: none; text-align: center; color: #4caf50; font-size: 18px; margin-top: 20px; font-weight: bold; }
107
  </style>
108
  </head>
109
  <body>
110
  <div class="container">
111
  <h2>๐Ÿšจ AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ (Overview)</h2>
112
-
113
  <div class="input-section">
114
  <input type="text" id="video_url" placeholder="์œ ํŠœ๋ธŒ ์˜์ƒ ๋งํฌ (์„ ํƒ์‚ฌํ•ญ)">
115
- <input type="text" id="product_url" placeholder="์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๋งํฌ (ํ•„์ˆ˜)" value="https://brand.naver.com/pacsafe/products/9365045491">
116
- <button onclick="analyzeAd()">๋ถ„์„ ์‹œ์ž‘ (๋ฐ์ดํ„ฐ ํฌ๋กค๋ง ๋ฐ AI ๋ถ„์„)</button>
117
  </div>
118
-
119
- <div id="loading">๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  AI ๋ชจ๋ธ์ด ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” โณ...</div>
120
-
121
  <div class="dashboard" id="dashboard">
122
- <div class="card score-card">
123
- <h3>ํ†ตํ•ฉ ์œ„ํ—˜๋„ ์ ์ˆ˜ (Final Score)</h3>
124
- <canvas id="scoreChart"></canvas>
125
- <h1 id="scoreText" style="margin-top: 15px;">0.00์ </h1>
126
- <p id="statusText" style="color: #a0a0a0;"></p>
127
- </div>
128
-
129
- <div class="card xai-card">
130
- <h3>๐Ÿค– AI ์ตœ์ข… ํŒ์ • ์‚ฌ๊ณ ํšŒ๋กœ (XAI Reasoning)</h3>
131
- <div id="xaiReasoning" style="margin-bottom: 20px;"></div>
132
-
133
- <h3>๐ŸŒ RAG ํŒฉํŠธ์ฒดํฌ 2D ๋ฒกํ„ฐ ๊ณต๊ฐ„ ๋น„๊ต</h3>
134
- <canvas id="vectorChart" style="max-height: 200px;"></canvas>
135
- </div>
136
  </div>
137
-
138
- <div class="dashboard" id="detailsDashboard" style="grid-template-columns: 1fr; margin-top: 0;">
139
  <div class="card">
140
- <h3>โš™๏ธ ์„ธ๋ถ€ ์—”์ง„ ๋ถ„์„ ๊ฒฐ๊ณผ (Detailed Engine Results)</h3>
141
  <div class="details-grid">
142
- <div class="detail-item">
143
- <h4>[X1] ํ˜•ํƒœ์†Œ ๋ฐ ๋‹จ์–ด ํƒ์ง€๊ธฐ</h4>
144
- <p id="x1Details"></p>
145
- </div>
146
- <div class="detail-item">
147
- <h4>[X2] ์˜๋ฏธ๋ก ์  ๋ฌธ๋งฅ ์œ ์‚ฌ๋„ (KoELECTRA)</h4>
148
- <p id="x2Details"></p>
149
- </div>
150
- <div class="detail-item" style="grid-column: span 2;">
151
- <h4>[X3] RAG ๊ธฐ๋ฐ˜ ํŒฉํŠธ์ฒดํฌ ๊ต์ฐจ ๊ฒ€์ฆ</h4>
152
- <p id="x3Details"></p>
153
- </div>
154
  </div>
155
  </div>
156
  </div>
157
  </div>
158
-
159
  <script>
160
- let scoreChartInstance = null;
161
- let vectorChartInstance = null;
162
-
163
  async function analyzeAd() {
164
  const videoUrl = document.getElementById('video_url').value;
165
  const productUrl = document.getElementById('product_url').value;
166
-
167
- if (!productUrl) return alert("์ƒํ’ˆ ๋งํฌ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค!");
168
-
169
  document.getElementById('loading').style.display = 'block';
170
- document.getElementById('dashboard').style.display = 'none';
171
- document.getElementById('detailsDashboard').style.display = 'none';
172
-
173
- try {
174
- const response = await fetch('/api/analyze', {
175
- method: 'POST',
176
- headers: { 'Content-Type': 'application/json' },
177
- body: JSON.stringify({ video_url: videoUrl, product_url: productUrl })
178
- });
179
-
180
- const data = await response.json();
181
- document.getElementById('loading').style.display = 'none';
182
-
183
- if (data.status === "success") {
184
- document.getElementById('dashboard').style.display = 'grid';
185
- document.getElementById('detailsDashboard').style.display = 'grid';
186
-
187
- // ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ
188
- document.getElementById('scoreText').innerText = data.final_score.toFixed(2) + "์ ";
189
- document.getElementById('statusText').innerText = data.message;
190
- document.getElementById('statusText').style.color = data.final_score > 70 ? "#ff5252" : (data.final_score > 40 ? "#ffeb3b" : "#4caf50");
191
-
192
- document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning;
193
- document.getElementById('x1Details').innerHTML = data.x1_details;
194
- document.getElementById('x2Details').innerHTML = data.x2_details;
195
- document.getElementById('x3Details').innerHTML = data.x3_details;
196
-
197
- // ๐Ÿ“Š ๋„๋„› ์ฐจํŠธ ๋ Œ๋”๋ง
198
- renderScoreChart(data.final_score);
199
-
200
- // ๐ŸŒ 2D ๋ฒกํ„ฐ ์‚ฐ์ ๋„ ๋ Œ๋”๋ง
201
- renderVectorChart(data.vector_data);
202
-
203
- } else {
204
- alert("๋ถ„์„ ์‹คํŒจ: " + data.error);
205
- }
206
- } catch (err) {
207
- document.getElementById('loading').style.display = 'none';
208
- alert("์„œ๋ฒ„ ํ†ต์‹  ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
209
- }
210
- }
211
-
212
- function renderScoreChart(score) {
213
- const ctx = document.getElementById('scoreChart').getContext('2d');
214
- if(scoreChartInstance) scoreChartInstance.destroy();
215
-
216
- const color = score > 70 ? '#ff5252' : (score > 40 ? '#ffeb3b' : '#4caf50');
217
-
218
- scoreChartInstance = new Chart(ctx, {
219
- type: 'doughnut',
220
- data: {
221
- labels: ['์œ„ํ—˜๋„', '์•ˆ์ „'],
222
- datasets: [{
223
- data: [score, 100 - score],
224
- backgroundColor: [color, '#2c2c2e'],
225
- borderWidth: 0
226
- }]
227
- },
228
- options: {
229
- cutout: '75%',
230
- plugins: { legend: { display: false } }
231
- }
232
- });
233
- }
234
-
235
- function renderVectorChart(vectorData) {
236
- const ctx = document.getElementById('vectorChart').getContext('2d');
237
- if(vectorChartInstance) vectorChartInstance.destroy();
238
-
239
- scoreChartInstance = new Chart(ctx, {
240
- type: 'scatter',
241
- data: {
242
- datasets: [
243
- {
244
- label: '์‹์•ฝ์ฒ˜ ๊ทœ์ • (Fact)',
245
- data: [{ x: 0, y: 0 }],
246
- backgroundColor: '#4caf50',
247
- pointRadius: 8
248
- },
249
- {
250
- label: '๊ด‘๊ณ  ๋ฌธ๊ตฌ (Claim)',
251
- data: [{ x: vectorData.x, y: vectorData.y }],
252
- backgroundColor: '#ff5252',
253
- pointRadius: 8
254
- }
255
- ]
256
- },
257
- options: {
258
- responsive: true,
259
- scales: {
260
- x: { grid: { color: '#333' }, min: -10, max: 100, title: {display: true, text: '์˜๋ฏธ๋ก ์  ๊ฑฐ๋ฆฌ (X)', color: '#888'} },
261
- y: { grid: { color: '#333' }, min: -10, max: 100, title: {display: true, text: '์˜๋ฏธ๋ก ์  ๊ฑฐ๋ฆฌ (Y)', color: '#888'} }
262
- },
263
- plugins: {
264
- legend: { labels: { color: '#fff' } }
265
- }
266
- }
267
  });
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
  </script>
270
  </body>
@@ -274,7 +125,7 @@ async def serve_frontend():
274
 
275
  # 2. ๋ถ„์„ API
276
  @app.post("/api/analyze")
277
- def api_analyze(req: AdRequest):
278
  try:
279
  # Step 0: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
280
  stt_text = ingestion.run_stt(ingestion.extract_audio_from_video(req.video_url)) if req.video_url.strip() else ""
@@ -284,70 +135,32 @@ def api_analyze(req: AdRequest):
284
  if len(combined_text) < 5:
285
  return {"status": "error", "error": "ํ…์ŠคํŠธ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."}
286
 
287
- # Step 1, 2, 3: ์ ์ˆ˜ ๋„์ถœ
288
  x1_score = lexical.calculate_x1_score(combined_text)
289
  x2_score = semantic.calculate_x2_score(combined_text)
290
  x3_score, matched_fact = rag_checker.calculate_x3_score(combined_text)
291
 
292
- # Step 4: ๋จธ์‹ ๋Ÿฌ๋‹ ์Šค์ฝ”์–ด๋ง
293
  final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
294
 
295
- # =====================================================================
296
- # ๐ŸŒŸ UI์— ๋ฟŒ๋ ค์ค„ ์ƒ์„ธ ์„ค๋ช…(Detail Text) ์ƒ์„ฑ ๋กœ์ง
297
- # =====================================================================
298
-
299
- # [X1 ์„ธ๋ถ€๊ฒฐ๊ณผ] ๋ฐœ๊ฒฌ๋œ ๊ธˆ์น™์–ด ์ถ”์ถœ
300
  detected_words = [word for word in lexical.lexicon.keys() if word in combined_text]
301
- if detected_words:
302
- x1_details = f"<span class='danger'>์ ๋ฐœ๋œ ๋‹จ์–ด: {', '.join(detected_words)}</span><br>์ด ๋‹จ์–ด๋“ค์€ ์‹์•ฝ์ฒ˜ ๊ฐ€์ด๋“œ๋ผ์ธ์— ์˜ํ•ด ์‚ฌ์šฉ์ด ๊ฐ•ํ•˜๊ฒŒ ๊ทœ์ œ๋˜๋Š” ํ‘œํ˜„์ž…๋‹ˆ๋‹ค."
303
- else:
304
- x1_details = "<span class='highlight'>๋ฐœ๊ฒฌ๋œ ๊ธˆ์น™์–ด ์—†์Œ.</span><br>๋ช…์‹œ์ ์ธ ํ—ˆ์œ„ ๊ณผ์žฅ ๋‹จ์–ด๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์•„ ํ…์ŠคํŠธ ํ‘œ๋ฉด์ ์œผ๋กœ๋Š” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค."
305
-
306
- # [X2 ์„ธ๋ถ€๊ฒฐ๊ณผ] ๋ฌธ๋งฅ ์„ค๋ช…
307
- if x2_score > 60:
308
- x2_details = f"<span class='danger'>๋ฌธ๋งฅ์  ์œ„ํ—˜๋„ {x2_score:.1f}์ </span><br>๋‹จ์ˆœ ๋‹จ์–ด๋ฅผ ๋„˜์–ด, ๋ฌธ์žฅ์˜ ์ „๋ฐ˜์ ์ธ ๋‰˜์•™์Šค๊ฐ€ ๊ณผ๊ฑฐ ์ ๋ฐœ๋œ ํ—ˆ์œ„๊ด‘๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ณผ์žฅ ํŒจํ„ด(๋‹จ์ •์  ํ‘œํ˜„, ํšจ๋Šฅ ๋งน์‹  ๋“ฑ)๊ณผ <b>๋งค์šฐ ์œ ์‚ฌํ•˜๊ฒŒ ๊ฐ์ง€</b>๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
309
- else:
310
- x2_details = f"<span class='highlight'>๋ฌธ๋งฅ์  ์œ„ํ—˜๋„ {x2_score:.1f}์ </span><br>๊ณผ๊ฑฐ ์ ๋ฐœ๋œ ํ—ˆ์œ„๊ด‘๊ณ  ํŠน์œ ์˜ ์ž๊ทน์ ์ด๊ฑฐ๋‚˜ ๊ธฐ๋งŒ์ ์ธ ๋ฌธ๋งฅ ํŒจํ„ด์ด ํฌ๊ฒŒ ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
311
-
312
- # [X3 ์„ธ๋ถ€๊ฒฐ๊ณผ] RAG ์„ค๋ช…
313
- x3_details = f"<b>[๊ด€๋ จ ์‹์•ฝ์ฒ˜ ๊ทœ์ • ๋งค์นญ]</b><br>{matched_fact}<br><br>"
314
- if x3_score > 50: # GPT๊ฐ€ ๋†’์€ ์œ„๋ฐ˜ ์ ์ˆ˜๋ฅผ ์ค€ ๊ฒฝ์šฐ
315
- x3_details += f"<b>[LLM ์ถ”๋ก  ๊ฒฐ๊ณผ: <span class='danger'>๋ชจ์ˆœ ๋ฐœ๊ฒฌ</span>]</b><br>๊ด‘๊ณ  ๋ฌธ๊ตฌ๊ฐ€ ์œ„ ์‹์•ฝ์ฒ˜ ๊ทœ์ •์„ ๋ช…๋ฐฑํžˆ ์œ„๋ฐ˜ํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ์ถ”๋ก ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
316
- else:
317
- x3_details += f"<b>[LLM ์ถ”๋ก  ๊ฒฐ๊ณผ: <span class='highlight'>๊ทœ์ • ์ค€์ˆ˜</span>]</b><br>๊ด‘๊ณ  ๋ฌธ๊ตฌ์™€ ์œ„ ์‹์•ฝ์ฒ˜ ๊ทœ์ • ๊ฐ„์— ์‹ฌ๊ฐํ•œ ๋…ผ๋ฆฌ์  ๋ชจ์ˆœ์ด๋‚˜ ์œ„๋ฐ˜ ์‚ฌํ•ญ์ด ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
318
-
319
- # [XAI ์‚ฌ๊ณ ํšŒ๋กœ] SHAP ๊ธฐ๋ฐ˜ ์„œ์ˆ 
320
- xai_reasoning = f"AI๋Š” ์ด ๊ด‘๊ณ  ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‚ฌ๊ณ  ๊ณผ์ •์„ ๊ฑฐ์ณค์Šต๋‹ˆ๋‹ค.<br><ul>"
321
- if shap_vals[0] > 0: xai_reasoning += f"<li>ํ‘œ๋ฉด์ ์ธ ๋‹จ์–ด(X1)์— ๊ณผ์žฅ๋œ ํ‘œํ˜„์ด ์„ž์—ฌ ์žˆ์–ด ์œ„ํ—˜๋„๋ฅผ ๋†’์˜€์Šต๋‹ˆ๋‹ค. <span class='danger'>(+{shap_vals[0]:.2f})</span></li>"
322
- else: xai_reasoning += f"<li>๊ธˆ์น™์–ด๊ฐ€ ๊ฒ€์ถœ๋˜์ง€ ์•Š์•„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๊ธ€๋กœ ์ธ์‹ํ–ˆ์Šต๋‹ˆ๋‹ค. <span class='highlight'>({shap_vals[0]:.2f})</span></li>"
323
 
324
- if shap_vals[1] > 0: xai_reasoning += f"<li>ํ•˜์ง€๋งŒ ๋ฌธ๋งฅ์˜ ๋‰˜์•™์Šค(X2)๊ฐ€ ๊ณผ๊ฑฐ ํ—ˆ์œ„๊ด‘๊ณ ์™€ ๋„ˆ๋ฌด ๋น„์Šทํ•ด AI๊ฐ€ ๊ฐ•ํ•œ ์˜์‹ฌ์„ ํ’ˆ์—ˆ์Šต๋‹ˆ๋‹ค. <span class='danger'>(+{shap_vals[1]:.2f})</span></li>"
325
- else: xai_reasoning += f"<li>์ „์ฒด์ ์ธ ๋ฌธ๋งฅ๊ณผ ๋‰˜์•™์Šค(X2) ์—ญ์‹œ ์ •์ƒ์ ์ธ ์ œํ’ˆ ์„ค๋ช…์˜ ํ˜•ํƒœ๋ฅผ ๋ ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. <span class='highlight'>({shap_vals[1]:.2f})</span></li>"
326
 
327
- if shap_vals[2] > 0: xai_reasoning += f"<li><b>๊ฒฐ์ •์ ์œผ๋กœ ์‹์•ฝ์ฒ˜ ํŒฉํŠธ์ฒดํฌ(X3)์—์„œ ๋ช…๋ฐฑํ•œ ๊ทœ์ • ์œ„๋ฐ˜์ด ํ™•์ธ</b>๋˜์–ด, ์ตœ์ข… ์œ„ํ—˜ ํŒ์ •์„ ๋‚ด๋ ธ์Šต๋‹ˆ๋‹ค. <span class='danger'>(+{shap_vals[2]:.2f})</span></li>"
328
- else: xai_reasoning += f"<li><b>์‹์•ฝ์ฒ˜ ํŒฉํŠธ์ฒดํฌ(X3) ๊ฒฐ๊ณผ ๊ทœ์ • ์œ„๋ฐ˜ ์†Œ์ง€๊ฐ€ ๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•„</b> ์ตœ์ข…์ ์œผ๋กœ ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํŒ๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. <span class='highlight'>({shap_vals[2]:.2f})</span></li>"
329
- xai_reasoning += "</ul>"
330
-
331
- # [๋ฒกํ„ฐ ๊ณต๊ฐ„ ์‹œ๊ฐํ™” ๋ฐ์ดํ„ฐ] ์œ ์‚ฌ๋„๊ฐ€ ๋‚ฎ์„์ˆ˜๋ก ๋‘ ์ ์˜ ๊ฑฐ๋ฆฌ๊ฐ€ ๋ฉ€์–ด์ง
332
- # ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„ ๏ฟฝ๏ฟฝ์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž„์˜์˜ x, y ์ขŒํ‘œ ์‚ฐ์ถœ
333
- distance = max(10, 100 - x2_score) # 0์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ์œ„ํ—˜(๊ฐ€๊นŒ์›€)
334
- vector_data = {"x": distance * 0.8, "y": distance * 0.9}
335
-
336
- message = "๐Ÿšจ ์•…์˜์ ์ธ ํ—ˆ์œ„/๊ณผ๋Œ€๊ด‘๊ณ ๋กœ ์˜์‹ฌ๋ฉ๋‹ˆ๋‹ค." if final_score > 70 else ("โš ๏ธ ์ผ๋ถ€ ๊ณผ์žฅ๋œ ๋‚ด์šฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." if final_score > 40 else "โœ… ๊ณผ๋Œ€๊ด‘๊ณ  ์†Œ์ง€๊ฐ€ ์ ์€ ์•ˆ์ „ํ•œ ์ฝ˜ํ…์ธ ์ž…๋‹ˆ๋‹ค.")
337
 
338
  return {
339
  "status": "success",
340
  "final_score": float(round(final_score, 2)),
341
- "message": message,
342
  "x1_details": x1_details,
343
  "x2_details": x2_details,
344
  "x3_details": x3_details,
345
- "xai_reasoning": xai_reasoning,
346
- "vector_data": vector_data
347
  }
348
-
349
  except Exception as e:
350
  return {"status": "error", "error": str(e)}
351
 
352
  if __name__ == "__main__":
 
353
  uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)
 
1
+ import os
 
 
 
 
2
  import math
3
+ import uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from fastapi import FastAPI
5
  from fastapi.responses import HTMLResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
  from pydantic import BaseModel
 
 
8
 
9
  # ==========================================
10
  # ๐ŸŒŸ AI ๋ ˆ๊ณ  ๋ธ”๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
11
  # ==========================================
12
+ # ํด๋” ๊ตฌ์กฐ์— ๋”ฐ๋ผ import ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
13
+ # myapp ํด๋” ์•ˆ์—์„œ ์‹คํ–‰๋œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์œ ์ง€ํ•˜์„ธ์š”.
14
  from step0_ingestion import DataIngestionPipeline
15
  from step1_lexical import LexicalAnalyzer
16
  from step2_semantic import SemanticAnalyzer
 
19
 
20
  app = FastAPI()
21
 
22
+ # ๐ŸŒŸ ์™ธ๋ถ€ ํ˜ธ์ถœ ํ—ˆ์šฉ (CORS ์„ค์ •)
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["*"],
26
+ allow_credentials=True,
27
+ allow_methods=["*"],
28
+ allow_headers=["*"],
29
+ )
30
+
31
  print("==================================================")
32
  print(" โณ AI ์—”์ง„ ๋ฐ ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ๋“ค์„ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฌ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...")
33
  print("==================================================")
34
+ # Render ๋ฌด๋ฃŒ ํ”Œ๋žœ(512MB)์—์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ,
35
+ # ์ด ์ดˆ๊ธฐํ™” ๋ถ€๋ถ„๋“ค์„ ํ•จ์ˆ˜ ๋‚ด๋ถ€๋กœ ์˜ฎ๊ฒจ์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
36
  ingestion = DataIngestionPipeline()
37
  lexical = LexicalAnalyzer()
38
  semantic = SemanticAnalyzer()
39
  rag_checker = FactCheckerRAG()
40
  xai_scorer = XAIScorer()
41
+ print("\nโœ… [์„œ๋ฒ„ ์ค€๋น„ ์™„๋ฃŒ]\n")
42
 
43
  class AdRequest(BaseModel):
44
  video_url: str
45
  product_url: str
46
 
47
+ # 1. ๐ŸŒŸ ๋Œ€์‹œ๋ณด๋“œ ์›น ํ”„๋ก ํŠธ์—”๋“œ (HTML)
48
  @app.get("/", response_class=HTMLResponse)
49
  async def serve_frontend():
50
+ # ์ œ๊ณตํ•ด์ฃผ์‹  HTML ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
51
  html_content = """
52
  <!DOCTYPE html>
53
  <html lang="ko">
 
59
  body { font-family: 'Pretendard', sans-serif; background-color: #121212; color: #ffffff; padding: 20px; margin: 0; }
60
  .container { max-width: 1200px; margin: auto; }
61
  h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #333; padding-bottom: 10px; }
 
 
62
  .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
63
  input { width: 100%; padding: 12px; margin: 8px 0; background: #2c2c2e; border: 1px solid #444; border-radius: 8px; color: #fff; box-sizing: border-box; }
64
  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; }
 
 
 
65
  .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
66
+ .card { background: #1e1e1e; padding: 20px; border-radius: 12px; }
67
+ #loading { display: none; text-align: center; color: #4caf50; font-size: 18px; margin-top: 20px; font-weight: bold; }
 
 
 
 
 
 
 
 
68
  .danger { color: #ff5252; font-weight: bold; }
69
+ .highlight { color: #4caf50; font-weight: bold; }
 
70
  .details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
71
  .detail-item { background: #2c2c2e; padding: 15px; border-radius: 8px; }
 
 
 
 
72
  </style>
73
  </head>
74
  <body>
75
  <div class="container">
76
  <h2>๐Ÿšจ AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ (Overview)</h2>
 
77
  <div class="input-section">
78
  <input type="text" id="video_url" placeholder="์œ ํŠœ๋ธŒ ์˜์ƒ ๋งํฌ (์„ ํƒ์‚ฌํ•ญ)">
79
+ <input type="text" id="product_url" placeholder="์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๋งํฌ (ํ•„์ˆ˜)">
80
+ <button onclick="analyzeAd()">๋ถ„์„ ์‹œ์ž‘</button>
81
  </div>
82
+ <div id="loading">๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...</div>
 
 
83
  <div class="dashboard" id="dashboard">
84
+ <div class="card"><h3>์œ„ํ—˜๋„</h3><canvas id="scoreChart"></canvas><h1 id="scoreText"></h1><p id="statusText"></p></div>
85
+ <div class="card"><h3>AI ์‚ฌ๊ณ ํšŒ๋กœ</h3><div id="xaiReasoning"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
+ <div class="dashboard" id="detailsDashboard" style="grid-template-columns: 1fr;">
 
88
  <div class="card">
89
+ <h3>์„ธ๋ถ€ ๋ถ„์„ ๊ฒฐ๊ณผ</h3>
90
  <div class="details-grid">
91
+ <div id="x1Details" class="detail-item"></div>
92
+ <div id="x2Details" class="detail-item"></div>
93
+ <div id="x3Details" class="detail-item" style="grid-column: span 2;"></div>
 
 
 
 
 
 
 
 
 
94
  </div>
95
  </div>
96
  </div>
97
  </div>
 
98
  <script>
 
 
 
99
  async function analyzeAd() {
100
  const videoUrl = document.getElementById('video_url').value;
101
  const productUrl = document.getElementById('product_url').value;
 
 
 
102
  document.getElementById('loading').style.display = 'block';
103
+ const response = await fetch('/api/analyze', {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ video_url: videoUrl, product_url: productUrl })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  });
108
+ const data = await response.json();
109
+ document.getElementById('loading').style.display = 'none';
110
+ if (data.status === "success") {
111
+ document.getElementById('dashboard').style.display = 'grid';
112
+ document.getElementById('detailsDashboard').style.display = 'grid';
113
+ document.getElementById('scoreText').innerText = data.final_score + "์ ";
114
+ document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning;
115
+ document.getElementById('x1Details').innerHTML = data.x1_details;
116
+ document.getElementById('x2Details').innerHTML = data.x2_details;
117
+ document.getElementById('x3Details').innerHTML = data.x3_details;
118
+ }
119
  }
120
  </script>
121
  </body>
 
125
 
126
  # 2. ๋ถ„์„ API
127
  @app.post("/api/analyze")
128
+ async def api_analyze(req: AdRequest):
129
  try:
130
  # Step 0: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
131
  stt_text = ingestion.run_stt(ingestion.extract_audio_from_video(req.video_url)) if req.video_url.strip() else ""
 
135
  if len(combined_text) < 5:
136
  return {"status": "error", "error": "ํ…์ŠคํŠธ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."}
137
 
 
138
  x1_score = lexical.calculate_x1_score(combined_text)
139
  x2_score = semantic.calculate_x2_score(combined_text)
140
  x3_score, matched_fact = rag_checker.calculate_x3_score(combined_text)
141
 
 
142
  final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
143
 
144
+ # UI์šฉ ํ…์ŠคํŠธ ์ƒ์„ฑ (์ œ๊ณตํ•ด์ฃผ์‹  ๋กœ์ง๊ณผ ๋™์ผ)
 
 
 
 
145
  detected_words = [word for word in lexical.lexicon.keys() if word in combined_text]
146
+ x1_details = f"์ ๋ฐœ ๋‹จ์–ด: {', '.join(detected_words)}" if detected_words else "๊ธˆ์น™์–ด ์—†์Œ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ x2_details = f"๋ฌธ๋งฅ ์œ„ํ—˜๋„: {x2_score:.1f}์ "
149
+ x3_details = f"์ฐธ์กฐ ๊ทœ์ •: {matched_fact}"
150
 
151
+ xai_reasoning = "AI ๋ถ„์„ ์™„๋ฃŒ" # ๊ฐ„๋‹จํ•˜๊ฒŒ ์ถ•์•ฝ
 
 
 
 
 
 
 
 
 
152
 
153
  return {
154
  "status": "success",
155
  "final_score": float(round(final_score, 2)),
 
156
  "x1_details": x1_details,
157
  "x2_details": x2_details,
158
  "x3_details": x3_details,
159
+ "xai_reasoning": xai_reasoning
 
160
  }
 
161
  except Exception as e:
162
  return {"status": "error", "error": str(e)}
163
 
164
  if __name__ == "__main__":
165
+ # ๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ
166
  uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)