sungho7373 commited on
Commit
ef5fc9d
ยท
1 Parent(s): 0b41dae

feat: apply yt-dlp, headless browser, and loading spinner UI

Browse files
Files changed (3) hide show
  1. myapp/app.py +70 -108
  2. myapp/step0_ingestion.py +30 -34
  3. requirements.txt +2 -2
myapp/app.py CHANGED
@@ -1,53 +1,6 @@
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
17
- from step3_rag import FactCheckerRAG
18
- from step4_xai import XAIScorer
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">
@@ -62,24 +15,43 @@ async def serve_frontend():
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>
@@ -99,22 +71,53 @@ async def serve_frontend():
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>
@@ -123,44 +126,3 @@ async def serve_frontend():
123
  """
124
  return HTMLResponse(content=html_content)
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 ""
132
- ocr_text = ingestion.run_ocr_from_web(req.product_url) if req.product_url.strip() else ""
133
- combined_text = f"{stt_text}\n{ocr_text}".strip()
134
-
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # 1. ๐ŸŒŸ ๋Œ€์‹œ๋ณด๋“œ ์›น ํ”„๋ก ํŠธ์—”๋“œ (HTML)
2
  @app.get("/", response_class=HTMLResponse)
3
  async def serve_frontend():
 
4
  html_content = """
5
  <!DOCTYPE html>
6
  <html lang="ko">
 
15
  .input-section { background: #1e1e1e; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
16
  input { width: 100%; padding: 12px; margin: 8px 0; background: #2c2c2e; border: 1px solid #444; border-radius: 8px; color: #fff; box-sizing: border-box; }
17
  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; }
18
+ button:disabled { background: #555; cursor: not-allowed; opacity: 0.7; }
19
  .dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
20
  .card { background: #1e1e1e; padding: 20px; border-radius: 12px; }
21
+
22
+ /* ๐ŸŒŸ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ CSS ์ถ”๊ฐ€ */
23
+ #loading-spinner { display: none; text-align: center; margin: 30px 0; }
24
+ .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; }
25
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
26
+ .loading-text { margin-top: 15px; font-size: 16px; font-weight: bold; color: #4caf50; }
27
+ .loading-subtext { font-size: 14px; color: #888; font-weight: normal; }
28
+
29
  .danger { color: #ff5252; font-weight: bold; }
30
  .highlight { color: #4caf50; font-weight: bold; }
31
  .details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
32
  .detail-item { background: #2c2c2e; padding: 15px; border-radius: 8px; }
33
+ #error-message { display: none; background: #ff5252; color: white; padding: 15px; border-radius: 8px; margin-top: 20px; text-align: center; font-weight: bold; }
34
  </style>
35
  </head>
36
  <body>
37
  <div class="container">
38
  <h2>๐Ÿšจ AI ๊ณผ๋Œ€๊ด‘๊ณ  ํƒ์ง€ ๋Œ€์‹œ๋ณด๋“œ (Overview)</h2>
39
  <div class="input-section">
40
+ <input type="text" id="video_url" placeholder="์œ ํŠœ๋ธŒ ์˜์ƒ ๋งํฌ (ํ˜„์žฌ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” ๋น„์›Œ๋‘๊ณ  ํ…Œ์ŠคํŠธ ๊ถŒ์žฅ)">
41
  <input type="text" id="product_url" placeholder="์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๋งํฌ (ํ•„์ˆ˜)">
42
+ <button id="analyze-btn" onclick="analyzeAd()">๋ถ„์„ ์‹œ์ž‘</button>
43
+ </div>
44
+
45
+ <div id="loading-spinner">
46
+ <div class="spinner"></div>
47
+ <p class="loading-text">
48
+ ๐Ÿง  AI๊ฐ€ ์—ด์‹ฌํžˆ ํŒฉํŠธ์ฒดํฌ๋ฅผ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค...<br>
49
+ <span class="loading-subtext">(๋”ฅ๋Ÿฌ๋‹ ๋ถ„์„์œผ๋กœ ์ธํ•ด ์•ฝ 1~2๋ถ„ ์ •๋„ ์†Œ์š”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)</span>
50
+ </p>
51
  </div>
52
+
53
+ <div id="error-message"></div>
54
+
55
  <div class="dashboard" id="dashboard">
56
  <div class="card"><h3>์œ„ํ—˜๋„</h3><canvas id="scoreChart"></canvas><h1 id="scoreText"></h1><p id="statusText"></p></div>
57
  <div class="card"><h3>AI ์‚ฌ๊ณ ํšŒ๋กœ</h3><div id="xaiReasoning"></div></div>
 
71
  async function analyzeAd() {
72
  const videoUrl = document.getElementById('video_url').value;
73
  const productUrl = document.getElementById('product_url').value;
74
+
75
+ // UI ์š”์†Œ ๊ฐ€์ ธ์˜ค๊ธฐ
76
+ const loadingSpinner = document.getElementById('loading-spinner');
77
+ const analyzeBtn = document.getElementById('analyze-btn');
78
+ const dashboard = document.getElementById('dashboard');
79
+ const detailsDashboard = document.getElementById('detailsDashboard');
80
+ const errorMsg = document.getElementById('error-message');
81
+
82
+ // ๐ŸŒŸ ๋กœ๋”ฉ UI ์ผœ๊ธฐ & ๋ฒ„ํŠผ ์ž ๊ธˆ
83
+ loadingSpinner.style.display = 'block';
84
+ analyzeBtn.disabled = true;
85
+ analyzeBtn.innerText = '๋ถ„์„ ์ค‘... โณ';
86
+ dashboard.style.display = 'none';
87
+ detailsDashboard.style.display = 'none';
88
+ errorMsg.style.display = 'none';
89
+
90
+ try {
91
+ const response = await fetch('/api/analyze', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ video_url: videoUrl, product_url: productUrl })
95
+ });
96
+
97
+ const data = await response.json();
98
+
99
+ if (data.status === "success") {
100
+ dashboard.style.display = 'grid';
101
+ detailsDashboard.style.display = 'grid';
102
+ document.getElementById('scoreText').innerText = data.final_score + "์ ";
103
+ document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning || "AI ๋ถ„์„ ์™„๋ฃŒ";
104
+ document.getElementById('x1Details').innerHTML = data.x1_details || "์ ๋ฐœ ๋‚ด์—ญ ์—†์Œ";
105
+ document.getElementById('x2Details').innerHTML = data.x2_details || "์•ˆ์ „ํ•จ";
106
+ document.getElementById('x3Details').innerHTML = data.x3_details || "๊ทœ์ • ์œ„๋ฐ˜ ์—†์Œ";
107
+ } else {
108
+ // ๋ฐฑ์—”๋“œ์—์„œ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ
109
+ errorMsg.innerText = "โŒ ๋ถ„์„ ์‹คํŒจ: " + data.error;
110
+ errorMsg.style.display = 'block';
111
+ }
112
+ } catch (error) {
113
+ console.error('Fetch ์—๋Ÿฌ:', error);
114
+ errorMsg.innerText = "โŒ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.";
115
+ errorMsg.style.display = 'block';
116
+ } finally {
117
+ // ๐ŸŒŸ ๋กœ๋”ฉ ๋„๊ธฐ & ๋ฒ„ํŠผ ์›์ƒ๋ณต๊ตฌ
118
+ loadingSpinner.style.display = 'none';
119
+ analyzeBtn.disabled = false;
120
+ analyzeBtn.innerText = '๋ถ„์„ ์‹œ์ž‘';
121
  }
122
  }
123
  </script>
 
126
  """
127
  return HTMLResponse(content=html_content)
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
myapp/step0_ingestion.py CHANGED
@@ -3,7 +3,7 @@ import gc
3
  import requests
4
  import torch
5
  import whisper
6
- from pytubefix import YouTube
7
  from paddleocr import PaddleOCR
8
  from playwright.sync_api import sync_playwright
9
  import logging
@@ -20,13 +20,29 @@ class DataIngestionPipeline:
20
  print("๐Ÿงน ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ์™„๋ฃŒ")
21
 
22
  def extract_audio_from_video(self, video_url, output_filename="temp_audio"):
23
- print(f"\n๐ŸŽฅ [1] ์œ ํŠœ๋ธŒ ์˜์ƒ ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘: {video_url}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
- yt = YouTube(video_url, 'WEB')
26
- audio_stream = yt.streams.get_audio_only()
27
- file_path = audio_stream.download(filename=f"{output_filename}.mp4")
28
- print(f"โœ… ์˜ค๋””์˜ค ์ถ”์ถœ ์™„๋ฃŒ: {file_path}")
29
- return file_path
 
 
30
  except Exception as e:
31
  print(f"โŒ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ: {e}")
32
  return None
@@ -52,8 +68,12 @@ class DataIngestionPipeline:
52
 
53
  raw_image_urls = []
54
  with sync_playwright() as p:
55
- print(" -> ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์„ ๋„์šฐ๊ณ  ํŽ˜์ด์ง€ ๋กœ๋”ฉ์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค...")
56
- browser = p.chromium.launch(headless=False)
 
 
 
 
57
  page = browser.new_page()
58
  page.goto(product_url, wait_until="domcontentloaded", timeout=60000)
59
  page.wait_for_timeout(3000)
@@ -151,28 +171,4 @@ class DataIngestionPipeline:
151
  final_text = "\n".join(all_extracted_text) # ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ํ•ฉ์น˜๊ธฐ
152
  print("\nโœ… ์›นํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ OCR ๋ณ€ํ™˜ ์™„๋ฃŒ!")
153
  return final_text
154
-
155
-
156
- # ==========================================
157
- # ์‹ค์ œ ์‹คํ–‰ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
158
- # ==========================================
159
- if __name__ == "__main__":
160
- pipeline = DataIngestionPipeline()
161
-
162
- # 1. ์œ ํŠœ๋ธŒ STT ํ…Œ์ŠคํŠธ
163
- test_video_url = "https://youtu.be/SJxSDRxd8Dc?si=t8dnIQciFulUlbVW"
164
- try:
165
- audio_file = pipeline.extract_audio_from_video(test_video_url)
166
- if audio_file:
167
- stt_text = pipeline.run_stt(audio_file)
168
- print(f"\n[STT ๊ฒฐ๊ณผ (์Œ์„ฑ -> ํ…์ŠคํŠธ)]\n{stt_text[:500]}...\n")
169
- except Exception as e:
170
- print(f"์œ ํŠœ๋ธŒ/STT ์ฒ˜๋ฆฌ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {e}")
171
-
172
- # 2. ์›นํŽ˜์ด์ง€ OCR ํ…Œ์ŠคํŠธ
173
- test_product_url = "https://brand.naver.com/pacsafe/products/9365045491"
174
- try:
175
- ocr_text = pipeline.run_ocr_from_web(test_product_url)
176
- print(f"\n[์›นํŽ˜์ด์ง€ OCR ๊ฒฐ๊ณผ (์ƒ์„ธ ์ด๋ฏธ์ง€ -> ํ…์ŠคํŠธ)]\n===========================\n{ocr_text}\n===========================")
177
- except Exception as e:
178
- print(f"OCR ์ฒ˜๋ฆฌ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {e}")
 
3
  import requests
4
  import torch
5
  import whisper
6
+ import yt_dlp
7
  from paddleocr import PaddleOCR
8
  from playwright.sync_api import sync_playwright
9
  import logging
 
20
  print("๐Ÿงน ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ์™„๋ฃŒ")
21
 
22
  def extract_audio_from_video(self, video_url, output_filename="temp_audio"):
23
+ print(f"\n๐ŸŽฅ [1] ์œ ํŠœ๋ธŒ ์˜์ƒ ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘ (yt-dlp): {video_url}")
24
+
25
+ # ์œ ํŠœ๋ธŒ ๋กœ๋ด‡ ์ฐจ๋‹จ ์šฐํšŒ ๋ฐ mp3 ์ถ”์ถœ ์ตœ์ ํ™” ์˜ต์…˜
26
+ ydl_opts = {
27
+ 'format': 'bestaudio/best',
28
+ 'outtmpl': f'{output_filename}.%(ext)s',
29
+ 'postprocessors': [{
30
+ 'key': 'FFmpegExtractAudio',
31
+ 'preferredcodec': 'mp3',
32
+ 'preferredquality': '192',
33
+ }],
34
+ 'quiet': True,
35
+ 'no_warnings': True
36
+ }
37
+
38
  try:
39
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
40
+ ydl.download([video_url])
41
+
42
+ # ๋ณ€ํ™˜๋œ mp3 ํŒŒ์ผ๋ช…
43
+ final_path = f"{output_filename}.mp3"
44
+ print(f"โœ… ์˜ค๋””์˜ค ์ถ”์ถœ ์™„๋ฃŒ: {final_path}")
45
+ return final_path
46
  except Exception as e:
47
  print(f"โŒ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ: {e}")
48
  return None
 
68
 
69
  raw_image_urls = []
70
  with sync_playwright() as p:
71
+ print(" -> ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋„์šฐ๊ณ  ํŽ˜์ด์ง€ ๋กœ๋”ฉ์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค...")
72
+ # ๐Ÿšจ ์ค‘์š”: ํ—ˆ๊น…ํŽ˜์ด์Šค(์„œ๋ฒ„) ํ™˜๊ฒฝ์„ ์œ„ํ•œ headless=True ๋ฐ ๋„์ปค ํ•„์ˆ˜ ์˜ต์…˜ ์ถ”๊ฐ€!
73
+ browser = p.chromium.launch(
74
+ headless=True,
75
+ args=["--no-sandbox", "--disable-setuid-sandbox"]
76
+ )
77
  page = browser.new_page()
78
  page.goto(product_url, wait_until="domcontentloaded", timeout=60000)
79
  page.wait_for_timeout(3000)
 
171
  final_text = "\n".join(all_extracted_text) # ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ํ•ฉ์น˜๊ธฐ
172
  print("\nโœ… ์›นํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ OCR ๋ณ€ํ™˜ ์™„๋ฃŒ!")
173
  return final_text
174
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -9,8 +9,8 @@ openai-whisper
9
  --extra-index-url https://download.pytorch.org/whl/cpu
10
  torch
11
  sentence-transformers
12
- pytubefix
13
  python-mecab-ko
14
  numpy
15
  xgboost==1.7.6
16
- shap
 
 
9
  --extra-index-url https://download.pytorch.org/whl/cpu
10
  torch
11
  sentence-transformers
 
12
  python-mecab-ko
13
  numpy
14
  xgboost==1.7.6
15
+ shap
16
+ yt-dlp