Spaces:
Sleeping
Sleeping
Commit ยท
6484389
1
Parent(s): 699791e
edit 5
Browse files- myapp/app.py +55 -242
myapp/app.py
CHANGED
|
@@ -1,45 +1,16 @@
|
|
| 1 |
-
|
| 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โ
[์๋ฒ ์ค๋น ์๋ฃ]
|
| 60 |
|
| 61 |
class AdRequest(BaseModel):
|
| 62 |
video_url: str
|
| 63 |
product_url: str
|
| 64 |
|
| 65 |
-
# 1. ๐
|
| 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;
|
| 89 |
-
|
| 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="์ํ ์์ธํ์ด์ง ๋งํฌ (ํ์)"
|
| 116 |
-
<button onclick="analyzeAd()">๋ถ์ ์์
|
| 117 |
</div>
|
| 118 |
-
|
| 119 |
-
<div id="loading">๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ AI ๋ชจ๋ธ์ด ๋ถ์ ์ค์
๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์ โณ...</div>
|
| 120 |
-
|
| 121 |
<div class="dashboard" id="dashboard">
|
| 122 |
-
<div class="card
|
| 123 |
-
|
| 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>
|
| 141 |
<div class="details-grid">
|
| 142 |
-
<div class="detail-item">
|
| 143 |
-
|
| 144 |
-
|
| 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 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 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 |
-
|
| 325 |
-
|
| 326 |
|
| 327 |
-
|
| 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)
|