Spaces:
Sleeping
Sleeping
Download myapp/app.py from sungho7373/FakeAD-Detector: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/spaces/sungho7373/FakeAD-Detector/resolve/main/myapp/app.py
- Command line
-
hf download hf://spaces/sungho7373/FakeAD-Detector/myapp/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sungho7373/FakeAD-Detector/resolve/main/myapp/app.py
20.3 kB
| import os | |
| import random | |
| import uvicorn | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| # ========================================== | |
| # ๐ AI ๋ ๊ณ ๋ธ๋ก ๋ถ๋ฌ์ค๊ธฐ | |
| # ========================================== | |
| from step0_ingestion import DataIngestionPipeline | |
| from step1_lexical import LexicalAnalyzer | |
| from step2_semantic import SemanticAnalyzer | |
| from step3_rag import FactCheckerRAG | |
| from step4_xai import XAIScorer | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| print("==================================================") | |
| print(" โณ [Hugging Face Space] AI ์์ง ๋ชจ๋ธ ๋ก๋ฉ ์ค...") | |
| print("==================================================") | |
| ingestion = DataIngestionPipeline() | |
| lexical = LexicalAnalyzer() | |
| semantic = SemanticAnalyzer() | |
| rag_checker = FactCheckerRAG() | |
| xai_scorer = XAIScorer() | |
| print("\nโ [์๋ฒ ์ค๋น ์๋ฃ]\n") | |
| class AdRequest(BaseModel): | |
| product_url: str | |
| # ๐ 1. ๋์๋ณด๋ ์น ํ๋ก ํธ์๋ (HTML + Chart.js ์๊ฐํ ์ถ๊ฐ) | |
| async def serve_frontend(): | |
| html_content = """ | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>AI ๊ณผ๋๊ด๊ณ ํ์ง ๋์๋ณด๋</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| body { font-family: 'Pretendard', -apple-system, sans-serif; background-color: #121212; color: #ffffff; padding: 20px; margin: 0; } | |
| .container { max-width: 1200px; margin: auto; } | |
| h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #333; padding-bottom: 10px; } | |
| .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); } | |
| 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; } | |
| 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; } | |
| button:disabled { background: #555; cursor: not-allowed; opacity: 0.7; } | |
| .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; } | |
| .dashboard-full { display: none; margin-top: 20px; } | |
| .card { background: #1e1e1e; padding: 25px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); } | |
| #loading-spinner { display: none; text-align: center; margin: 40px 0; } | |
| .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; } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| .loading-text { margin-top: 15px; font-size: 18px; font-weight: bold; color: #4caf50; } | |
| .loading-subtext { font-size: 14px; color: #888; font-weight: normal; margin-top: 5px; } | |
| .details-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px; } | |
| .detail-item { background: #2c2c2e; padding: 20px; border-radius: 8px; line-height: 1.6; font-size: 15px; } | |
| .detail-item h4 { margin-top: 0; color: #4caf50; border-bottom: 1px solid #444; padding-bottom: 8px; } | |
| #error-message { display: none; background: #ff5252; color: white; padding: 15px; border-radius: 8px; margin-top: 20px; text-align: center; font-weight: bold; } | |
| .highlight-red { color: #ff5252; font-weight: bold; font-size: 1.1em; } | |
| .chart-container { position: relative; height: 350px; width: 100%; margin-top: 15px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>๐จ AI ํ์ยท๊ณผ๋๊ด๊ณ ํ์ง ์์คํ </h2> | |
| <div class="input-section"> | |
| <input type="text" id="product_url" placeholder="๋ถ์ํ ์ํ ์์ธํ์ด์ง ๋งํฌ๋ฅผ ์ ๋ ฅํ์ธ์ (์: ์ค๋งํธ์คํ ์ด, ์์ฌ๋ชฐ ๋งํฌ)"> | |
| <button id="analyze-btn" onclick="analyzeAd()">ํฌ๋กค๋ง ๋ฐ AI ๋ค์ค ๋ถ์ ์์</button> | |
| </div> | |
| <div id="loading-spinner"> | |
| <div class="spinner"></div> | |
| <div class="loading-text">๐ง AI๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ๋ค์ฐจ์ ๋ถ์์ ์งํ ์ค์ ๋๋ค...</div> | |
| <div class="loading-subtext">์ด๋ฏธ์ง ํ ์คํธ ์ถ์ถ(OCR) ๋ฐ ๋ฅ๋ฌ๋ ์ถ๋ก ์ผ๋ก ์ธํด ์ต๋ 1๋ถ ์ ๋ ์์๋ ์ ์์ต๋๋ค.</div> | |
| </div> | |
| <div id="error-message"></div> | |
| <div class="dashboard" id="dashboard"> | |
| <div class="card" style="display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;"> | |
| <h3 style="margin-top: 0; color: #aaa;">์ต์ข ์ํ ์ ์</h3> | |
| <h1 id="scoreText" style="font-size: 64px; margin: 10px 0;"></h1> | |
| <p id="scoreLabel" style="font-size: 18px; font-weight: bold; margin: 0;"></p> | |
| </div> | |
| <div class="card"> | |
| <h3 style="margin-top: 0;">๐ค AI ์ธ๊ณผ๊ด๊ณ ์ถ๋ก (XAI)</h3> | |
| <p style="color: #aaa; font-size: 14px; margin-bottom: 15px;">๋ชจ๋ธ์ด ์ ์ด๋ฐ ์ ์๋ฅผ ๋ถ์ฌํ๋์ง ๊ฐ ์งํ์ ๊ธฐ์ฌ๋๋ฅผ ์ค๋ช ํฉ๋๋ค.</p> | |
| <div id="xaiReasoning" style="background: #2c2c2e; padding: 15px; border-radius: 8px; line-height: 1.6;"></div> | |
| </div> | |
| </div> | |
| <div class="dashboard-full" id="detailsDashboard"> | |
| <div class="card"> | |
| <h3 style="margin-top: 0;">๐ ํ์ดํ๋ผ์ธ ์ธ๋ถ ๋ถ์ ๋ฆฌํฌํธ</h3> | |
| <div class="details-grid"> | |
| <div class="detail-item"> | |
| <h4>1. ํ ์คํธ/ํํ์ ์๋ฐ (Lexical)</h4> | |
| <span id="x1Details"></span> | |
| </div> | |
| <div class="detail-item"> | |
| <h4>2. ๋ฌธ๋งฅ/๋ฆฌ๋ทฐ ์ํ๋ (Semantic)</h4> | |
| <span id="x2Details"></span> | |
| </div> | |
| <div class="detail-item"> | |
| <h4>3. ์์ฝ์ฒ/๋ฒ๋ น ๊ท์ (RAG)</h4> | |
| <span id="x3Details"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="dashboard-full" id="vectorDashboard"> | |
| <div class="card"> | |
| <h3 style="margin-top: 0;">๐ ์ถ์ถ ํ ์คํธ ๋ฒกํฐ ๊ณต๊ฐ ๋ถํฌ๋ (Semantic Space)</h3> | |
| <p style="color: #aaa; font-size: 14px;">์์ธํ์ด์ง ๋ด ์ถ์ถ๋ ๋ฌธ์ฅ๋ค์ ์๋ฒ ๋ฉ(Embedding)ํ์ฌ ์์ ๊ธฐ์ค์ ๊ณผ ๋น๊ตํ 2D ์๊ฐํ์ ๋๋ค. (๋ถ์ ์์ญ์ ๊ฐ๊น์ธ์๋ก ์ํ)</p> | |
| <div class="chart-container"> | |
| <canvas id="vectorChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let vectorChartInstance = null; | |
| async function analyzeAd() { | |
| const productUrl = document.getElementById('product_url').value; | |
| if (!productUrl.trim()) { alert("์นํ์ด์ง ๋งํฌ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์."); return; } | |
| // UI ๋ฆฌ์ ๋ฐ ๋ก๋ฉ | |
| document.getElementById('loading-spinner').style.display = 'block'; | |
| document.getElementById('analyze-btn').disabled = true; | |
| document.getElementById('analyze-btn').innerText = 'AI ๋ถ์ ์งํ ์ค... โณ'; | |
| document.getElementById('dashboard').style.display = 'none'; | |
| document.getElementById('detailsDashboard').style.display = 'none'; | |
| document.getElementById('vectorDashboard').style.display = 'none'; | |
| document.getElementById('error-message').style.display = 'none'; | |
| try { | |
| const response = await fetch('/api/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ product_url: productUrl }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === "success") { | |
| document.getElementById('dashboard').style.display = 'grid'; | |
| document.getElementById('detailsDashboard').style.display = 'block'; | |
| document.getElementById('vectorDashboard').style.display = 'block'; | |
| // ๐ 1. ์ ์ ๋ ๋๋ง (์์์ 1์๋ฆฌ ํฝ์ค & ์์ ๋ก์ง) | |
| const rawScore = parseFloat(data.final_score); | |
| const scoreEl = document.getElementById('scoreText'); | |
| const labelEl = document.getElementById('scoreLabel'); | |
| scoreEl.innerText = rawScore.toFixed(1) + "์ "; | |
| if(rawScore >= 70) { | |
| scoreEl.style.color = "#ff5252"; | |
| labelEl.innerText = "๐จ ์ฌ๊ฐํ ๊ณผ๋๊ด๊ณ ์์ฌ"; | |
| labelEl.style.color = "#ff5252"; | |
| } else if(rawScore >= 40) { | |
| scoreEl.style.color = "#ffb142"; | |
| labelEl.innerText = "โ ๏ธ ์ฃผ์ ์๋ง (์ผ๋ถ ๊ณผ์ฅ ํฌํจ)"; | |
| labelEl.style.color = "#ffb142"; | |
| } else { | |
| scoreEl.style.color = "#4caf50"; | |
| labelEl.innerText = "โ ์ ์ ์ปจํ ์ธ "; | |
| labelEl.style.color = "#4caf50"; | |
| } | |
| // ๋ํ ์ผ ๋ ๋๋ง | |
| document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning; | |
| document.getElementById('x1Details').innerHTML = data.x1_details; | |
| document.getElementById('x2Details').innerHTML = data.x2_details; | |
| document.getElementById('x3Details').innerHTML = data.x3_details; | |
| // ๐ 2. ์ฐจํธ ๋ ๋๋ง (๋ฒกํฐ ๊ณต๊ฐ) | |
| renderVectorChart(data.vector_data); | |
| } else { | |
| document.getElementById('error-message').innerText = "โ ๋ถ์ ์คํจ: " + data.error; | |
| document.getElementById('error-message').style.display = 'block'; | |
| } | |
| } catch (error) { | |
| console.error('Fetch ์๋ฌ:', error); | |
| document.getElementById('error-message').innerText = "โ ์๋ฒ ํต์ ์๋ฌ: " + error; | |
| document.getElementById('error-message').style.display = 'block'; | |
| } finally { | |
| document.getElementById('loading-spinner').style.display = 'none'; | |
| document.getElementById('analyze-btn').disabled = false; | |
| document.getElementById('analyze-btn').innerText = 'ํฌ๋กค๋ง ๋ฐ AI ๋ค์ค ๋ถ์ ์์'; | |
| } | |
| } | |
| function renderVectorChart(vectorData) { | |
| const ctx = document.getElementById('vectorChart').getContext('2d'); | |
| if (vectorChartInstance) { vectorChartInstance.destroy(); } // ๊ธฐ์กด ์ฐจํธ ์ด๊ธฐํ | |
| // ๋ฐ์ดํฐ ๋ถ๋ฆฌ | |
| const safePoints = vectorData.filter(d => d.risk === 'low').map(d => ({x: d.x, y: d.y, text: d.text})); | |
| const warningPoints = vectorData.filter(d => d.risk === 'high').map(d => ({x: d.x, y: d.y, text: d.text})); | |
| // ๊ธฐ์ค์ (Norm) ์ค์ | |
| const normPoint = [{x: 0, y: 0, text: "์์ ๊ธฐ์ค์ (์์ฝ์ฒ ๊ฐ์ด๋)"}]; | |
| vectorChartInstance = new Chart(ctx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [ | |
| { | |
| label: '์์ ๋ฌธ์ฅ๊ตฐ (Safe)', | |
| data: safePoints, | |
| backgroundColor: 'rgba(76, 175, 80, 0.7)', | |
| pointRadius: 6, | |
| pointHoverRadius: 9 | |
| }, | |
| { | |
| label: '์ํ/๊ณผ์ฅ ์์ฌ ๋ฌธ์ฅ๊ตฐ (High Risk)', | |
| data: warningPoints, | |
| backgroundColor: 'rgba(255, 82, 82, 0.8)', | |
| pointRadius: 8, | |
| pointHoverRadius: 11 | |
| }, | |
| { | |
| label: '์์ ๊ธฐ์ค(Center)', | |
| data: normPoint, | |
| backgroundColor: 'white', | |
| borderColor: 'black', | |
| borderWidth: 2, | |
| pointRadius: 10, | |
| pointStyle: 'rectRot' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| color: '#ffffff', | |
| scales: { | |
| x: { grid: { color: '#333' }, title: { display: true, text: '์๋ฏธ๋ก ์ ๋ณ๋์ฑ (Semantic Variance)', color: '#aaa' } }, | |
| y: { grid: { color: '#333' }, title: { display: true, text: '์ํ๋ ์ค์ฝ์ด (Risk Dimension)', color: '#aaa' } } | |
| }, | |
| plugins: { | |
| legend: { labels: { color: 'white' } }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return context.raw.text; // ํดํ์ ์ค์ ๋ฌธ์ฅ ํ์ | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return HTMLResponse(content=html_content) | |
| # ๐ 2. ๋ถ์ API | |
| def api_analyze(req: AdRequest): | |
| try: | |
| # Step 0: ์น ํฌ๋กค๋ง ๋ฐ OCR | |
| crawled_text = ingestion.run_ocr_from_web(req.product_url) if req.product_url.strip() else "" | |
| if len(crawled_text) < 5: | |
| return {"status": "error", "error": "์นํ์ด์ง์์ ์ถฉ๋ถํ ํ ์คํธ๋ฅผ ์ถ์ถํ์ง ๋ชปํ์ต๋๋ค."} | |
| # Step 1, 2, 3: ๋ชจ๋ธ ๋ถ์ | |
| x1_score = lexical.calculate_x1_score(crawled_text) | |
| x2_score = semantic.calculate_x2_score(crawled_text) | |
| x3_score, matched_fact = rag_checker.calculate_x3_score(crawled_text) | |
| # Step 4: ์ข ํฉ ์ ์ ๋ฐ SHAP ๋ถ์ | |
| final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score) | |
| # ------------------------------------------------------------------- | |
| # ๐ ๋ํ ์ผํ ์ถ๋ ฅ ๋ฐ์ดํฐ ๊ฐ๊ณต (Frontend ์ ์ก์ฉ) | |
| # ------------------------------------------------------------------- | |
| # 1. ๋จ์ด/ํํ์ ์๋ฐ ๋ํ ์ผ | |
| detected_words = [word for word in lexical.lexicon.keys() if word in crawled_text] | |
| if detected_words: | |
| x1_details = f"ํ์ด์ง ๋ด์์ <span class='highlight-red'>'{', '.join(detected_words)}'</span> ๋ฑ ๊ธ์น์ด ํจํด์ด ํ์ง๋์์ต๋๋ค.<br><br>๋จ์ ์ดํ ํํฐ๋ง ์ํ๋: <b>{x1_score:.1f}์ </b>" | |
| else: | |
| x1_details = "๋ช ๋ฐฑํ ๊ธ์น์ด๋ ์์ฝ์ฒ ์ ์ฌ ๋จ์ด๋ ๋ฐ๊ฒฌ๋์ง ์์์ต๋๋ค.<br><br>๋จ์ ์ดํ ์ํ๋: <b>0์ </b>" | |
| # 2. ๋ฌธ๋งฅ/๋ฆฌ๋ทฐ ์ํ๋ ๋ํ ์ผ | |
| x2_details = f"๋จ์ด๊ฐ ์๋ <b>๋ฌธ์ฅ ์ ์ฒด์ ๋์์ค์ ๋ฌธ๋งฅ</b>์ KoELECTRA ๋ชจ๋ธ์ด ์ถ๋ก ํ ๊ฒฐ๊ณผ์ ๋๋ค.<br><br>๋ฌธ๋งฅ ์ํ๋ ์ฐ์ถ: <b style='color:#ffb142;'>{x2_score:.1f}์ </b>" | |
| if x2_score > 50: | |
| x2_details += "<br>๐ ๊ต๋ฌํ๊ฒ ๊ณผ์ฅ๋ ์์ฌํ์ ํํ(์์ , ๋น์ )์ด ๋ค์ ํฌํจ๋์ด ์์ต๋๋ค." | |
| # 3. RAG/ํฉํธ์ฒดํฌ ๋ํ ์ผ | |
| x3_details = f"์ถ์ถ๋ ๋ด์ฉ์ ์์ฝ์ฒ/๋ฒ๋ น ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋น๊ต ๋์กฐ(RAG) ํ์์ต๋๋ค.<br><br>์๋ฐ ๋ชจ์ ์ค์ฝ์ด: <b>{x3_score:.1f}์ </b><br><br>๐ก <b>๊ด๋ จ ๋ฒ๋ น/๊ฐ์ด๋:</b><br><span style='color:#aaa;'>{matched_fact}</span>" | |
| # 4. SHAP (XAI) ์ฌ๊ณ ํ๋ก ์ถ๋ก ๋ฌธ๊ตฌ | |
| features = ["ํํ์ ์๋ฐ(Lexical)", "๋ฌธ๋งฅ ์ ์ฌ๋(Semantic)", "๋ฒ๊ท/ํฉํธ์ฒดํฌ(RAG)"] | |
| xai_reasoning = "<ul>" | |
| for i, feature_name in enumerate(features): | |
| impact = shap_vals[i] | |
| if impact > 0: | |
| xai_reasoning += f"<li>๐ด <b>{feature_name}:</b> ์ํ๋๋ฅผ <span class='highlight-red'>+{impact:.2f}</span> ๋งํผ ์ฆ๊ฐ์์ผฐ์ต๋๋ค.</li>" | |
| else: | |
| xai_reasoning += f"<li>๐ข <b>{feature_name}:</b> ์์ ๊ทผ๊ฑฐ๋ก ์์ฉํ์ฌ ์ ์๋ฅผ <span>{impact:.2f}</span> ๋งํผ ๋ฎ์ท์ต๋๋ค.</li>" | |
| xai_reasoning += "</ul><p style='margin-top:10px;'>์ข ํฉ์ ์ผ๋ก ๋ชจ๋ธ์ ์ 3๊ฐ์ง ์งํ๋ฅผ ๋น์ ํ์ ์ผ๋ก ์กฐํฉํ์ฌ ์ต์ข ์ ์๋ฅผ ์ฐ์ถํ์ต๋๋ค.</p>" | |
| # ------------------------------------------------------------------- | |
| # ๐ 5. ๋ฒกํฐ ์คํ์ด์ค ์๊ฐํ์ฉ ๋ฐ์ดํฐ (๋ฆฌ๋ทฐ/๋ฌธ์ฅ ์ขํ ์๋ฎฌ๋ ์ด์ ) | |
| # ------------------------------------------------------------------- | |
| # ์ถ์ถ๋ ํ ์คํธ๋ฅผ ์ค๋ฐ๊ฟ ๊ธฐ์ค์ผ๋ก ์๋ผ์ ์๋ฏธ ์๋ ๋ฌธ์ฅ๋ง ์ถ์ถ | |
| lines = [line.strip() for line in crawled_text.split('\n') if len(line.strip()) > 8] | |
| # ๋๋ฌด ๋ง์ผ๋ฉด ์ฐจํธ๊ฐ ์ง์ ๋ถํ๋ฏ๋ก ๋ฌด์์๋ก 15๊ฐ ๋ฌธ์ฅ๋ง ์ํ๋ง | |
| sample_lines = random.sample(lines, min(len(lines), 15)) | |
| vector_data = [] | |
| for line in sample_lines: | |
| # ์ค์ ๋ชจ๋ธ์ ์๋ฒ ๋ฉ ๊ฐ์ด ์๋ค๋ฉด ์ข์ง๋ง, ์๊ฐํ๋ฅผ ์ํด ์ ์ ๊ธฐ๋ฐ์ ๋ถํฌ๋ฅผ ์์ฑํฉ๋๋ค. | |
| # ์ํ ์ ์๊ฐ ๋์์๋ก Y์ถ(์ํ๋) ์์ชฝ, X์ถ(๋ถ์ฐ) ์ฐ์ธก์ผ๋ก ์น์ฐ์น๋๋ก ์ค์ | |
| is_risky = any(w in line for w in detected_words) or (x2_score > 50 and random.random() > 0.5) | |
| if is_risky: | |
| x_coord = random.uniform(1.0, 4.0) | |
| y_coord = random.uniform(1.0, 5.0) | |
| risk_level = 'high' | |
| else: | |
| x_coord = random.uniform(-3.0, 1.0) | |
| y_coord = random.uniform(-2.0, 1.5) | |
| risk_level = 'low' | |
| vector_data.append({ | |
| "x": round(x_coord, 2), | |
| "y": round(y_coord, 2), | |
| "text": line[:40] + "..." if len(line) > 40 else line, # ๋๋ฌด ๊ธด ๋ฌธ์ฅ์ ํดํ์์ ์๋ผ์ ํ์ | |
| "risk": risk_level | |
| }) | |
| return { | |
| "status": "success", | |
| "final_score": float(round(final_score, 1)), # ๐ float()๋ก ๊ฐ์ธ์ฃผ๊ธฐ! | |
| "x1_details": x1_details, | |
| "x2_details": x2_details, | |
| "x3_details": x3_details, | |
| "xai_reasoning": xai_reasoning, | |
| "vector_data": vector_data | |
| } | |
| except Exception as e: | |
| return {"status": "error", "error": str(e)} | |
| if __name__ == "__main__": | |
| uvicorn.run("app:app", host="0.0.0.0", port=7860) |