Sodagraph commited on
Commit
eda02a7
·
1 Parent(s): 617c2c9

스페이스 이동

Browse files
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vue.js
2
+ frontend/node_modules
3
+ frontend/dist
4
+
5
+ # Python
6
+ backend/app/__pycache__
7
+ backend/.pytest_cache
8
+ backend/.venv
9
+ backend/venv
10
+ backend/embeddings/ # RAG에서 생성된 임베딩 파일 저장 시
11
+ backend/.env
12
+ backend/app/temp_captions
13
+ backend/app/debug_yt_dlp.log
14
+
15
+ # html
16
+ static
17
+
18
+ # Docker
19
+ .dockerignore
20
+
21
+ # OS
22
+ .DS_Store
23
+ Thumbs.db
24
+ .env
25
+
26
+ # proxy rotation files
27
+ [internal]
28
+ =
29
+ transferring
Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Stage 1: Frontend Build ---
2
+ FROM node:20-slim as frontend-builder
3
+
4
+ WORKDIR /app/frontend
5
+
6
+ COPY frontend/package*.json ./
7
+
8
+ RUN npm install
9
+
10
+ COPY frontend/ .
11
+
12
+ RUN npm run build
13
+
14
+
15
+ # --- Stage 2: Backend and Combined Serving via FastAPI (Uvicorn) ---
16
+ FROM python:3.11-slim-bookworm
17
+
18
+ WORKDIR /app
19
+
20
+ # 시스템 패키지 업데이트 및 build-essential (Python 패키지 빌드용) 설치
21
+ RUN apt-get update && apt-get install -y --no-install-recommends \
22
+ build-essential \
23
+ curl \
24
+ git \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # Python 종속성 설치
28
+ COPY backend/requirements.txt ./backend/requirements.txt
29
+ RUN pip install --no-cache-dir -r backend/requirements.txt
30
+
31
+ # 백엔드 소스 코드 복사
32
+ COPY backend/ ./backend/
33
+
34
+ # Hugging Face 캐시 디렉토리 설정: /tmp는 일반적으로 쓰기 가능
35
+ ENV TRANSFORMERS_CACHE="/tmp/hf_cache"
36
+ ENV HF_HOME="/tmp/hf_cache"
37
+
38
+ # 프론트엔드 빌드 결과물을 백엔드 앱이 접근할 수 있는 경로로 복사합니다.
39
+ # /app/static 폴더를 만들고 그 안에 Vue.js 빌드 결과물을 넣습니다.
40
+ RUN mkdir -p /app/static
41
+ COPY --from=frontend-builder /app/frontend/dist /app/static
42
+
43
+ # 최종 실행 명령어
44
+ # 애플리케이션 포트 설정 (허깅페이스 스페이스는 7860번 포트를 사용)
45
+ EXPOSE 7860
46
+
47
+ # Uvicorn을 사용하여 FastAPI 앱을 실행합니다.
48
+ # main:app은 backend/app/main.py 파일의 app 객체를 의미합니다.
49
+ # --host 0.0.0.0 은 모든 네트워크 인터페이스에서 접근 가능하게 합니다.
50
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860} --app-dir backend/app"]
README.md CHANGED
@@ -1,11 +1,10 @@
1
  ---
2
- title: YouTube Transcript Extraction
3
- emoji: 🐢
4
- colorFrom: blue
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
8
- short_description: 'Enabling Keyword Search within Youtube Video Transcripts:'
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Po
3
+ emoji: 🚀
4
+ colorFrom: indigo
5
+ colorTo: red
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
backend/app/__init__.py ADDED
File without changes
backend/app/main.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/app/main.py
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.staticfiles import StaticFiles # StaticFiles 임포트
6
+ import os
7
+ from pydantic import BaseModel
8
+
9
+ from youtube_parser import process_youtube_video_data
10
+ from rag_core import perform_rag_query
11
+
12
+ app = FastAPI()
13
+
14
+ # CORS 설정: 프론트엔드와 백엔드가 다른 포트에서 실행될 때 필요
15
+ origins = [
16
+ "http://localhost:8080", # Vue 개발 서버 기본 포트
17
+ "http://localhost:5173", # Vue Vite 개발 서버 기본 포트
18
+ "https://sodagraph-po.hf.space", # 여러분의 Hugging Face Space URL
19
+ ]
20
+
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=origins,
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # 경로 설정
30
+ current_file_dir = os.path.dirname(os.path.abspath(__file__))
31
+ project_root_dir = os.path.join(current_file_dir, "..", "..")
32
+ static_files_dir = os.path.join(project_root_dir, "static")
33
+
34
+ # ✅ API 라우터 먼저 정의
35
+ @app.get("/api/health")
36
+ async def api_health_check():
37
+ return {"status": "ok", "message": "Backend API is running"}
38
+
39
+ class VideoProcessRequest(BaseModel):
40
+ video_url: str
41
+ query: str
42
+
43
+ @app.post("/api/process_youtube_video")
44
+ async def process_youtube_video(request: VideoProcessRequest):
45
+ try:
46
+ processed_chunks_with_timestamps = await process_youtube_video_data(request.video_url)
47
+
48
+ if not processed_chunks_with_timestamps:
49
+ return {"message": "자막 또는 내용을 추출할 수 없습니다.", "results": []}
50
+
51
+ rag_results = await perform_rag_query(
52
+ chunks_with_timestamps=processed_chunks_with_timestamps,
53
+ query=request.query,
54
+ top_k=5
55
+ )
56
+
57
+ return {
58
+ "status": "success",
59
+ "message": "성공적으로 영상을 처리하고 RAG 검색을 수행했습니다.",
60
+ "video_url": request.video_url,
61
+ "query": request.query,
62
+ "results": rag_results
63
+ }
64
+
65
+ except Exception as e:
66
+ print(f"ERROR: 서버 처리 중 오류 발생: {str(e)}")
67
+ raise HTTPException(status_code=500, detail="서버 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.")
68
+
69
+ # ✅ 정적 파일은 마지막에 mount
70
+ app.mount("/", StaticFiles(directory=static_files_dir, html=True), name="static")
71
+
72
+ # 서버 실행을 위한 메인 진입점 (Docker에서는 Uvicorn이 직접 호출하므로 필수는 아님)
73
+ if __name__ == "__main__":
74
+ import uvicorn
75
+ import os
76
+
77
+ port = int(os.environ.get("PORT", 7860)) # Hugging Face가 전달하는 포트를 우선 사용
78
+ uvicorn.run(app, host="0.0.0.0", port=port)
backend/app/proxy_manager.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/app/proxy_manager.py
2
+
3
+ import os
4
+ import requests # requests는 FreeProxy 테스트나 ScrapingBee API 직접 호출 시 유용
5
+ from loguru import logger
6
+ from dotenv import load_dotenv
7
+
8
+ # .env 파일에서 환경 변수 로드
9
+ load_dotenv()
10
+
11
+ # ScrapingBee API 키 (Proxy Mode를 사용한다면 필요 없을 수 있음)
12
+ SCRAPINGBEE_API_KEY = os.getenv("SCRAPINGBEE_API_KEY")
13
+
14
+ # ScrapingBee Proxy Mode 주소 (대시보드에서 확인한 실제 프록시 서버 주소)
15
+ SCRAPINGBEE_PROXY_ADDRESS = os.getenv("SCRAPINGBEE_PROXY_ADDRESS")
16
+
17
+ # FreeProxy 라이브러리 (필요하다면 주석 해제)
18
+ # from fp.fp import FreeProxy
19
+ # import asyncio # FreeProxy가 비동기로 동작한다면 필요
20
+
21
+ async def get_proxy_url() -> str | None:
22
+ """
23
+ 현재 설정된 프록시 제공자로부터 프록시 URL을 가져옵니다.
24
+ 나중에 다른 프록시 제공자로 변경 시 이 함수 내부만 수정하면 됩니다.
25
+ """
26
+ # ----------------------------------------------------
27
+ # ScrapingBee Proxy Mode 사용 예시 (추천)
28
+ # ScrapingBee 대시보드에서 얻은 프록시 주소를 사용합니다.
29
+ # .env 파일에 SCRAPINGBEE_PROXY_ADDRESS="us.scrapingbee.com:8880" 형태로 설정
30
+ if SCRAPINGBEE_PROXY_ADDRESS:
31
+ logger.info(f"ScrapingBee Proxy Mode 프록시 사용 시도: {SCRAPINGBEE_PROXY_ADDRESS}")
32
+ return SCRAPINGBEE_PROXY_ADDRESS
33
+ # ----------------------------------------------------
34
+
35
+ # ----------------------------------------------------
36
+ # FreeProxy 사용 예시 (폴백 또는 다른 선택지)
37
+ # 위에서 ScrapingBee 프록시를 찾지 못했거나 사용하지 않으려는 경우
38
+ # 주석을 해제하고 FreeProxy를 사용하도록 설정할 수 있습니다.
39
+ # try:
40
+ # logger.info("FreeProxy를 사용하여 무료 프록시 가져오기 시도...")
41
+ # # Hugging Face Spaces 허용 포트 (80, 443, 8080)만 필터링
42
+ # proxy_address = FreeProxy(timeout=1, port=[80, 443, 8080], https=True, rand=True).get()
43
+ # if proxy_address:
44
+ # logger.info(f"FreeProxy 사용: {proxy_address}")
45
+ # return proxy_address
46
+ # else:
47
+ # logger.warning("사용 가능한 FreeProxy를 찾을 수 없습니다.")
48
+ # except Exception as e:
49
+ # logger.warning(f"FreeProxy 가져오기 실패: {e}")
50
+ # ----------------------------------------------------
51
+
52
+ # 어떤 프록시도 설정되지 않았거나 가져오지 못한 경우
53
+ logger.warning("어떤 유효한 프록시도 구성되거나 가져올 수 없습니다. 프록시 없이 진행합니다.")
54
+ return None
backend/app/rag_core.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/app/rag_core.py
2
+
3
+ from sentence_transformers import SentenceTransformer
4
+ import faiss
5
+ import numpy as np
6
+ from typing import List, Dict, Tuple
7
+
8
+ # 전역 변수로 모델 로드 (앱 시작 시 한 번만 로드되도록)
9
+ # 네이버 클로바의 한국어 SentenceBERT 모델을 로드합니다.
10
+ try:
11
+ # 네이버 클로바 HyperCLOVAX-SEED-Text-Instruct-0.5B 모델 로드
12
+ model = SentenceTransformer('jhgan/ko-sroberta-multitask')
13
+ print("INFO: 임베딩 모델 'jhgan/ko-sroberta-multitask' 로드 완료.")
14
+ except Exception as e:
15
+ print(f"ERROR: 임베딩 모델 'jhgan/ko-sroberta-multitask' 로드 실패: {e}. 다국어 모델로 시도합니다.")
16
+ # 대체 모델 로직 (필요하다면 유지하거나 제거할 수 있습니다)
17
+ try:
18
+ model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
19
+ print("INFO: 임베딩 모델 'sentence-transformers/paraphrase-multilingual-L12-v2' 로드 완료.")
20
+ except Exception as e:
21
+ print(f"ERROR: 대체 임베딩 모델 로드 실패: {e}. RAG 기능을 사용할 수 없습니다.")
22
+ raise
23
+
24
+ async def perform_rag_query(chunks_with_timestamps: List[Dict], query: str, top_k: int = 5) -> List[Dict]:
25
+ """
26
+ 제공된 텍스트 청크들과 쿼리를 사용하여 RAG(Retrieval-Augmented Generation) 검색을 수행합니다.
27
+ 현재는 임베딩 기반 유사도 검색만 수행하며, LLM 호출은 추후 추가됩니다.
28
+
29
+ Args:
30
+ chunks_with_timestamps: [{"text": "...", "timestamp": "...", "start_seconds": ...}] 형태의 리스트.
31
+ query: 사용자 쿼리 문자열.
32
+ top_k: 쿼리와 가장 유사한 상위 N개의 청크를 반환.
33
+
34
+ Returns:
35
+ 쿼리와 가장 관련성 높은 상위 N개의 청크 (Dict) 리스트.
36
+ """
37
+ if not chunks_with_timestamps:
38
+ print("WARNING: RAG 검색을 위한 텍스트 청크가 없습니다.")
39
+ return []
40
+
41
+ # 1. 텍스트 임베딩 생성
42
+ # 모든 청크의 텍스트만 추출
43
+ texts = [chunk["text"] for chunk in chunks_with_timestamps]
44
+ print(f"INFO: 총 {len(texts)}개의 텍스트 청크 임베딩 시작.")
45
+
46
+ # 모델의 encode 메서드는 비동기가 아니므로, 직접 호출.
47
+ # 만약 시간이 오래 걸린다면 FastAPI의 `run_in_threadpool` 등을 고려.
48
+ try:
49
+ chunk_embeddings = model.encode(texts, convert_to_numpy=True)
50
+ print("INFO: 텍스트 청크 임베딩 완료.")
51
+ except Exception as e:
52
+ print(f"ERROR: 텍스트 청크 임베딩 중 오류 발생: {e}")
53
+ return []
54
+
55
+ # 2. FAISS 인덱스 생성 및 청크 임베딩 추가
56
+ dimension = chunk_embeddings.shape[1] # 임베딩 벡터의 차원
57
+ index = faiss.IndexFlatL2(dimension) # L2 유클리드 거리를 사용하는 간단한 인덱스
58
+ index.add(chunk_embeddings)
59
+ print("INFO: FAISS 인덱스 생성 및 임베딩 추가 완료.")
60
+
61
+ # 3. 쿼리 임베딩
62
+ query_embedding = model.encode([query], convert_to_numpy=True)
63
+ print("INFO: 쿼리 임베딩 완료.")
64
+
65
+ # 4. 유사도 검색 (FAISS)
66
+ # D: 거리 (Distance), I: 인덱스 (Index)
67
+ distances, indices = index.search(query_embedding, top_k)
68
+ print(f"INFO: FAISS 유사도 검색 완료. 상위 {top_k}개 결과.")
69
+
70
+ retrieved_chunks = []
71
+ for i, idx in enumerate(indices[0]):
72
+ if idx >= 0: # 유효한 인덱스만 (FAISS가 -1을 반환할 수 있음)
73
+ original_chunk = chunks_with_timestamps[idx]
74
+ retrieved_chunks.append({
75
+ "text": original_chunk["text"],
76
+ "timestamp": original_chunk["timestamp"],
77
+ "score": float(distances[0][i]) # 유사도 점수 (거리가 작을수록 유사)
78
+ })
79
+
80
+ # 거리가 작은 순서(유사도가 높은 순서)로 정렬하여 반환
81
+ retrieved_chunks.sort(key=lambda x: x['score'])
82
+
83
+ print(f"DEBUG: 최종 검색된 청크 수: {len(retrieved_chunks)}")
84
+ return retrieved_chunks
85
+
86
+ # ... (나머지 perform_rag_query 함수 코드는 동일합니다) ...
87
+
88
+ # 테스트용 코드 (직접 실행 시)
89
+ if __name__ == "__main__":
90
+ import asyncio
91
+
92
+ sample_chunks = [
93
+ {"text": "안녕하세요, 오늘 우리는 AI와 머신러닝의 미래에 대해 이야기할 것입니다.", "timestamp": "00:00:05", "start_seconds": 5},
94
+ {"text": "특히 딥러닝과 신경망이 어떻게 혁신을 이끄는지 살펴보겠습니다.", "timestamp": "00:00:15", "start_seconds": 15},
95
+ {"text": "이번 영상에서는 유튜브 영상 데이트 서비스 개발 과정을 보여드립니다.", "timestamp": "00:00:25", "start_seconds": 25},
96
+ {"text": "파이썬과 FastAPI를 사용하여 백엔드를 구축하고 있습니다.", "timestamp": "00:00:35", "start_seconds": 35},
97
+ {"text": "데이트 앱 개발은 쉬운 일이 아니지만, 재미있습니다.", "timestamp": "00:00:45", "start_seconds": 45},
98
+ {"text": "이 모델은 자���어 처리(NLP) 작업에 매우 유용합니다.", "timestamp": "00:00:55", "start_seconds": 55},
99
+ {"text": "다음주에는 새로운 데이트 장소를 탐험할 계획입니다.", "timestamp": "01:00:05", "start_seconds": 65},
100
+ ]
101
+
102
+ async def main():
103
+ print("\n[테스트 1] 쿼리: 데이트 앱")
104
+ query1 = "데이트 앱"
105
+ results1 = await perform_rag_query(sample_chunks, query1, top_k=3)
106
+ for r in results1:
107
+ print(f" [{r['score']:.4f}] {r['timestamp']}: {r['text']}")
108
+
109
+ print("\n[테스트 2] 쿼리: 인공지능")
110
+ query2 = "인공지능"
111
+ results2 = await perform_rag_query(sample_chunks, query2, top_k=2)
112
+ for r in results2:
113
+ print(f" [{r['score']:.4f}] {r['timestamp']}: {r['text']}")
114
+
115
+ asyncio.run(main())
backend/app/youtube_parser.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import contextlib
3
+ import asyncio
4
+ import json # json 모듈 추가
5
+ import shutil
6
+ import sys # sys 모듈 추가
7
+ from urllib.parse import urlparse, parse_qs
8
+
9
+ from loguru import logger
10
+ from yt_dlp import YoutubeDL
11
+ from proxy_manager import get_proxy_url
12
+ from dotenv import load_dotenv
13
+
14
+ # 환경 변수 로드 (코드의 가장 위에 위치)
15
+ load_dotenv()
16
+
17
+ # --- Loguru 설정 시작 ---
18
+ # 기본 핸들러(콘솔 출력) 제거
19
+ logger.remove()
20
+
21
+ # 콘솔 출력 핸들러: INFO 레벨 이상만 출력, 색상 적용, 간결한 포맷
22
+ logger.add(
23
+ sys.stderr, # 표준 에러 스트림 (콘솔)
24
+ level="INFO",
25
+ colorize=True,
26
+ format="<green>{time:HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
27
+ )
28
+
29
+ # 파일 출력 핸들러: DEBUG 레벨 이상 모든 로그를 파일로 저장, 10MB마다 새 파일 생성
30
+ logger.add(
31
+ "debug_yt_dlp.log", # 로그 파일 이름
32
+ level="DEBUG",
33
+ rotation="10 MB", # 파일 크기가 10MB가 되면 새 파일 생성
34
+ compression="zip", # 압축하여 저장 (선택 사항)
35
+ enqueue=True, # 비동기 로깅 (성능 향상)
36
+ format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}"
37
+ )
38
+ # --- Loguru 설정 끝 ---
39
+
40
+
41
+ async def get_youtube_video_id(url: str) -> str | None:
42
+ """
43
+ 유튜브 URL에서 비디오 ID를 추출합니다.
44
+ 표준 유튜브 URL (youtube.com/watch?v=..., youtu.be/...)을 처리합니다.
45
+ """
46
+ parsed_url = urlparse(url)
47
+
48
+ # 표준 YouTube Watch 페이지 도메인 확인
49
+ # www.youtube.com, m.youtube.com, youtube.com 등을 포함합니다.
50
+ # 'www.youtube.com', 'm.youtube.com', 'youtube.com'은 실제 YouTube 도메인을 의미합니다.
51
+ if parsed_url.hostname and any(domain in parsed_url.hostname for domain in ['www.youtube.com', 'm.youtube.com', 'youtube.com']):
52
+ query_params = parse_qs(parsed_url.query)
53
+ if 'v' in query_params:
54
+ return query_params['v'][0]
55
+
56
+ # 짧은 YouTube URL (youtu.be/VIDEO_ID)
57
+ elif parsed_url.hostname == 'youtu.be':
58
+ # path가 /VIDEO_ID 형태이므로 맨 앞의 '/'를 제거
59
+ video_id = parsed_url.path.strip('/')
60
+ # 유튜브 비디오 ID는 보통 11자리이므로, 유효성 검사
61
+ if len(video_id) == 11:
62
+ return video_id
63
+
64
+ logger.warning(f"알 수 없는 형식의 YouTube URL: {url}")
65
+ return None
66
+
67
+ async def get_transcript_with_timestamps(video_id: str) -> list[dict] | None:
68
+ logger.info(f"비디오 ID '{video_id}'에 대한 자막 가져오기 시도.")
69
+
70
+ processed_chunks = []
71
+ proxy_address = await get_proxy_url()
72
+
73
+ # yt-dlp 옵션 설정
74
+ ydl_opts = {
75
+ 'writesubtitles': True, # 사용자가 업로드한 수동 자막 파일 쓰기 활성화
76
+ 'writeautomaticsub': True, # YouTube에서 자동으로 생성된 자막 파일 쓰기 활성화
77
+ 'subtitleslangs': ['ko', 'en'], # 다운로드할 자막 언어 목록 (한국어 우선, 없으면 영어)
78
+ 'skip_download': True, # 동영상 자체는 다운로드하지 않고 자막만 다운로드
79
+ 'outtmpl': '%(id)s.%(language)s.%(ext)s', # 다운로드될 파일 이름 템플릿 (temp_dir 안에서 상대 경로로 저장됨)
80
+ 'quiet': False, # 콘솔 출력 활성화 (디버깅용)
81
+ 'no_warnings': False, # 경고 메시지 활성화 (디버깅용)
82
+ 'extractor_args': { # 특정 extractor (예: 유튜브)에 대한 추가 인자
83
+ 'youtube': {'skip': ['dash']} # dash manifest 관련 오류 회피 시도 (유튜브 관련)
84
+ }
85
+ # 프록시가 필요한 경우, 'proxy': 'http://your.proxy.com:port' 형태로 여기에 추가됩니다.
86
+ }
87
+
88
+ if proxy_address:
89
+ ydl_opts['proxy'] = proxy_address
90
+ logger.info(f"yt-dlp에 프록시 적용: {proxy_address}")
91
+ else:
92
+ logger.info("yt-dlp에 프록시가 적용되지 않았습니다.")
93
+
94
+ temp_dir = "./temp_captions"
95
+ os.makedirs(temp_dir, exist_ok=True)
96
+ original_cwd = os.getcwd()
97
+
98
+ try:
99
+ with contextlib.chdir(temp_dir): # 임시 디렉토리로 작업 디렉토리 변경
100
+ # outtmpl은 현재 chdir된 디렉토리 내의 상대 경로로 지정
101
+ # yt-dlp가 파일을 temp_dir 안에 바로 생성하도록 함
102
+ ydl_opts['outtmpl'] = '%(id)s.%(ext)s'
103
+
104
+ logger.debug(f"yt-dlp 실행 전 현재 작업 디렉토리: {os.getcwd()}")
105
+ logger.debug(f"yt-dlp 옵션: {ydl_opts}")
106
+
107
+ with YoutubeDL(ydl_opts) as ydl:
108
+ # download=True 설정으로 자막 다운로드 시도
109
+ # 비디오 ID만 전달해도 yt-dlp가 알아서 처리합니다.
110
+ info_dict = await asyncio.to_thread(ydl.extract_info, video_id, download=True)
111
+
112
+ logger.debug(f"yt-dlp extract_info 결과 (자세한 정보는 debug_yt_dlp.log 파일 확인): {json.dumps(info_dict.get('requested_subtitles', 'No subtitles requested'), indent=2, ensure_ascii=False)}")
113
+
114
+ caption_file_path = None
115
+
116
+ # 1. info_dict에서 직접 자막 파일 경로를 찾으려는 시도 (가장 정확)
117
+ # yt-dlp 0.0.12 버전 이상에서는 _download_lock이 반환됨. info_dict에서 직접 파일을 찾아야 함
118
+ if 'requested_subtitles' in info_dict and info_dict['requested_subtitles']:
119
+ for lang_code in ydl_opts['subtitleslangs']:
120
+ if lang_code in info_dict['requested_subtitles']:
121
+ sub_info = info_dict['requested_subtitles'][lang_code]
122
+ # 'filepath' 키가 없거나 None일 수 있으므로 확인
123
+ if 'filepath' in sub_info and sub_info['filepath']:
124
+ # filepath는 이미 현재 작업 디렉토리(temp_dir) 기준으로 되어 있을 것
125
+ caption_file_path = sub_info['filepath']
126
+ logger.info(f"yt-dlp가 '{lang_code}' 자막 파일을 info_dict에서 찾았습니다: {caption_file_path}")
127
+ break # 찾았으면 루프 종료
128
+
129
+ # 2. info_dict에서 찾지 못했을 경우, 폴백으로 임시 디렉토리를 탐색
130
+ if not caption_file_path:
131
+ logger.debug(f"info_dict에서 자막 파일 경로를 찾지 못했습니다. 임시 디렉토리 스캔 시작.")
132
+ downloaded_files = [f for f in os.listdir('.') if f.startswith(video_id) and ('sub' in f or 'vtt' in f or 'json' in f or 'srt' in f)]
133
+ logger.debug(f"임시 디렉토리의 파일 목록: {downloaded_files}")
134
+
135
+ # 한국어 자막 우선 검색 (vtt, srt, json 순)
136
+ for ext in ['vtt', 'srt', 'json']:
137
+ ko_file = next((f for f in downloaded_files if f.endswith(f'.ko.{ext}')), None)
138
+ if ko_file:
139
+ caption_file_path = os.path.join(os.getcwd(), ko_file) # 현재 작업 디렉토리 기준으로 경로 조합
140
+ logger.info(f"폴백: yt-dlp로 한국어 {ext.upper()} 자막 파일 '{ko_file}'을 다운로드했습니다.")
141
+ break
142
+
143
+ if not caption_file_path:
144
+ # 한국어 없으면 첫 번째 사용 가능한 자막 찾기
145
+ for ext in ['vtt', 'srt', 'json']:
146
+ any_file = next((f for f in downloaded_files if f.endswith(f'.{ext}')), None)
147
+ if any_file:
148
+ caption_file_path = os.path.join(os.getcwd(), any_file) # 현재 작업 디렉토리 기준으로 경로 조합
149
+ logger.warning(f"폴백: 한국어 자막이 없어 첫 번째 {ext.upper()} 자막 파일 '{any_file}'을 사용합니다.")
150
+ break
151
+
152
+ # 3. 자막 파일이 찾아졌으면 파싱 시작
153
+ if caption_file_path and os.path.exists(caption_file_path):
154
+ # VTT, SRT, JSON 등 다양한 자막 파일 형식에 대한 파싱 로직 분기
155
+ if caption_file_path.endswith('.vtt'):
156
+ with open(caption_file_path, 'r', encoding='utf-8') as f:
157
+ vtt_content = f.read()
158
+
159
+ # WEBVTT 파싱
160
+ segments = vtt_content.split('\n\n')
161
+ for segment in segments:
162
+ if '-->' in segment: # 시간 정보 포함 세그먼트
163
+ lines = segment.split('\n')
164
+ time_str = lines[0].strip()
165
+ text_content = ' '.join(lines[1:]).strip()
166
+
167
+ try:
168
+ # VTT 시간은 HH:MM:SS.ms 형태로 제공되므로, 밀리초를 float로 처리 후 정수로 변환
169
+ start_time_parts = time_str.split(' --> ')[0].split(':')
170
+ if len(start_time_parts) == 3: # HH:MM:SS.ms
171
+ hours = int(start_time_parts[0])
172
+ minutes = int(start_time_parts[1])
173
+ seconds = int(float(start_time_parts[2].split('.')[0]))
174
+ elif len(start_time_parts) == 2: # MM:SS.ms
175
+ hours = 0
176
+ minutes = int(start_time_parts[0])
177
+ seconds = int(float(start_time_parts[1].split('.')[0]))
178
+ else:
179
+ raise ValueError("Unsupported time format")
180
+
181
+ # HH:MM:SS 포맷으로 맞춤
182
+ if hours > 0:
183
+ timestamp_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
184
+ else:
185
+ timestamp_str = f"{minutes:02d}:{seconds:02d}"
186
+
187
+ processed_chunks.append({
188
+ "text": text_content,
189
+ "timestamp": timestamp_str
190
+ })
191
+ except Exception as e:
192
+ logger.warning(f"VTT 시간 파싱 오류: {time_str} - {e}")
193
+
194
+ logger.info(f"yt-dlp로 VTT 자막 {len(processed_chunks)}개 청크 처리 완료.")
195
+
196
+ elif caption_file_path.endswith('.json'):
197
+ # JSON 자막 파싱 (yt-dlp가 가끔 JSON 포맷으로도 다운로드함)
198
+ with open(caption_file_path, 'r', encoding='utf-8') as f:
199
+ json_content = json.load(f)
200
+
201
+ # yt-dlp의 JSON 자막 형식에 맞춰 파싱 (예시, 실제 구조는 info_dict를 통해 확인 필요)
202
+ for entry in json_content:
203
+ if 'start' in entry and 'text' in entry:
204
+ total_seconds = int(entry['start'])
205
+ hours = total_seconds // 3600
206
+ minutes = (total_seconds % 3600) // 60
207
+ seconds = total_seconds % 60
208
+
209
+ if hours > 0:
210
+ timestamp_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
211
+ else:
212
+ timestamp_str = f"{minutes:02d}:{seconds:02d}"
213
+
214
+ processed_chunks.append({
215
+ "text": entry['text'],
216
+ "timestamp": timestamp_str
217
+ })
218
+ logger.info(f"yt-dlp로 JSON 자막 {len(processed_chunks)}개 청크 처리 완료.")
219
+
220
+ elif caption_file_path.endswith('.srt'):
221
+ # SRT 자막 파싱 (간단한 예시, 실제로는 정규식 등으로 파싱)
222
+ with open(caption_file_path, 'r', encoding='utf-8') as f:
223
+ srt_content = f.read()
224
+
225
+ # SRT 파싱 로직 (매우 간단한 예시, 실제론 srt 라이브러리 사용 권장)
226
+ blocks = srt_content.strip().split('\n\n')
227
+ for block in blocks:
228
+ lines = block.split('\n')
229
+ # 최소한 순번, 시간, 텍스트가 있어야 함
230
+ if len(lines) >= 3 and '-->' in lines[1]:
231
+ time_str = lines[1].strip()
232
+ text_content = ' '.join(lines[2:]).strip()
233
+
234
+ try:
235
+ # SRT 시간은 HH:MM:SS,ms 형태로 제공
236
+ start_time_parts = time_str.split(' --> ')[0].split(':')
237
+ seconds_ms = float(start_time_parts[-1].replace(',', '.')) # 밀리초 처리
238
+ seconds = int(seconds_ms)
239
+ minutes = int(start_time_parts[-2])
240
+ hours = int(start_time_parts[0]) if len(start_time_parts) == 3 else 0
241
+
242
+ if hours > 0:
243
+ timestamp_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
244
+ else:
245
+ timestamp_str = f"{minutes:02d}:{seconds:02d}"
246
+
247
+ processed_chunks.append({
248
+ "text": text_content,
249
+ "timestamp": timestamp_str
250
+ })
251
+ except Exception as e:
252
+ logger.warning(f"SRT 시간 파싱 오류: {time_str} - {e}")
253
+ logger.info(f"yt-dlp로 SRT 자막 {len(processed_chunks)}개 청크 처리 완료.")
254
+
255
+ else:
256
+ logger.warning(f"지원하지 않는 자막 파일 형식입니다: {caption_file_path}")
257
+
258
+ else:
259
+ logger.warning(f"비디오 ID '{video_id}'에 대한 yt-dlp 자막 파일을 찾을 수 없습니다. 최종 시도 경로: {caption_file_path}")
260
+
261
+ except Exception as e:
262
+ logger.error(f"yt-dlp 자막 추출 중 예기치 않은 오류 발생 for video ID '{video_id}': {type(e).__name__}: {e}")
263
+ return []
264
+ finally:
265
+ if os.path.exists(temp_dir):
266
+ for file_name in os.listdir(temp_dir):
267
+ file_path = os.path.join(temp_dir, file_name)
268
+ try:
269
+ if os.path.isfile(file_path):
270
+ os.remove(file_path)
271
+ except Exception as e:
272
+ logger.error(f"임시 파일 삭제 실패 {file_path}: {e}")
273
+ os.rmdir(temp_dir)
274
+ logger.info(f"임시 자막 디렉토리 '{temp_dir}' 정리 완료.")
275
+ os.chdir(original_cwd) # 원래 작업 디렉토리로 돌아옴
276
+
277
+ return processed_chunks
278
+
279
+ def parse_srt_content(srt_content: str) -> list[dict]:
280
+ chunks = []
281
+ # 간단한 SRT 파싱 로직 (yt-dlp의 SRT 출력은 더 간단할 수 있음)
282
+ # 실제 프로덕션에서는 더 견고한 SRT 파서 라이브러리를 사용하는 것이 좋습니다.
283
+ import re
284
+ # SRT 패턴: 1\n00:00:01,000 --> 00:00:03,000\nHello World\n\n
285
+ blocks = re.split(r'\n\s*\n', srt_content.strip())
286
+ for block in blocks:
287
+ lines = block.split('\n')
288
+ if len(lines) >= 3:
289
+ # 첫 번째 라인은 순번, 두 번째 라인은 시간, 나머지는 텍스트
290
+ time_str = lines[1]
291
+ text = " ".join(lines[2:]).strip()
292
+
293
+ # 시간 형식: 00:00:01,000 --> 00:00:03,000
294
+ time_parts = time_str.split(' --> ')
295
+ if len(time_parts) == 2:
296
+ start_time = time_parts[0].replace(',', '.') # yt-dlp의 VTT 파서와 일관성을 위해 쉼표를 점으로 변경
297
+ chunks.append({"text": text, "timestamp": start_time})
298
+ return chunks
299
+
300
+
301
+ def parse_vtt_content(vtt_content: str) -> list[dict]:
302
+ chunks = []
303
+ lines = vtt_content.split('\n')
304
+ i = 0
305
+ while i < len(lines):
306
+ line = lines[i].strip()
307
+ if '-->' in line:
308
+ # 시간 정보 라인
309
+ time_str = line.split(' ')[0] # 예: 00:00:01.000
310
+ # 다음 라인부터 텍스트 시작
311
+ text_lines = []
312
+ j = i + 1
313
+ while j < len(lines) and lines[j].strip() != '':
314
+ text_lines.append(lines[j].strip())
315
+ j += 1
316
+ text = ' '.join(text_lines)
317
+ if text:
318
+ chunks.append({"text": text, "timestamp": time_str})
319
+ i = j # 다음 자막 블록으로 이동
320
+ i += 1
321
+ return chunks
322
+
323
+ def parse_json_content(json_content: dict) -> list[dict]:
324
+ chunks = []
325
+ for entry in json_content.get('events', []):
326
+ if 'segs' in entry:
327
+ text = "".join([seg.get('utf8', '') for seg in entry['segs']])
328
+ # JSON3 형식은 밀리초까지 표현된 시작 시간이 't' 키에 있을 수 있음
329
+ # yt-dlp가 생성하는 json3 파일 구조에 따라 유연하게 처리 필요
330
+ start_ms = entry.get('t', 0)
331
+ # 밀리초를 HH:MM:SS.mmm 형식으로 변환 (yt-dlp의 VTT timestamp와 유사하게)
332
+ total_seconds = start_ms / 1000
333
+ hours = int(total_seconds // 3600)
334
+ minutes = int((total_seconds % 3600) // 60)
335
+ seconds = total_seconds % 60
336
+ timestamp = f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"
337
+ chunks.append({"text": text, "timestamp": timestamp})
338
+ return chunks
339
+
340
+ async def process_youtube_video_data(video_url: str) -> list[dict] | None:
341
+ video_id = await get_youtube_video_id(video_url)
342
+ if not video_id:
343
+ logger.error(f"유효하지 않은 YouTube URL: {video_url}")
344
+ return None
345
+
346
+ return await get_transcript_with_timestamps(video_id)
backend/dependencies.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // backend/dependencies.json
2
+ {
3
+ "fastapi": "FastAPI (웹 프레임워크)",
4
+ "uvicorn": "Uvicorn (FastAPI 서버)",
5
+ "python-dotenv": "환경 변수 관리",
6
+ "youtube-transcript-api": "유튜브 자막 추출",
7
+ "langchain": "RAG 프레임워크 (LangChain)",
8
+ "langchain-community": "LangChain 커뮤니티 모듈 (문서 로더, 벡터스토어 등)",
9
+ "sentence-transformers": "임베딩 모델",
10
+ "chromadb": "벡터 데이터베이스 (경량)",
11
+ "transformers": "LLM 로드 (Hugging Face Transformers)",
12
+ "accelerate": "LLM 메모리 최적화",
13
+ "torch": "PyTorch (트랜스포머 라이브러리 의존성)",
14
+ "faiss-cpu": "FAISS (벡터 검색 라이브러리 - CPU 버전)"
15
+ }
backend/requirements.txt ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==0.30.1
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.12.13
4
+ aiosignal==1.3.2
5
+ annotated-types==0.7.0
6
+ anyio==4.9.0
7
+ asgiref==3.8.1
8
+ attrs==25.3.0
9
+ backoff==2.2.1
10
+ bcrypt==4.3.0
11
+ build==1.2.2.post1
12
+ cachetools==5.5.2
13
+ certifi==2025.6.15
14
+ charset-normalizer==3.4.2
15
+ chroma-hnswlib==0.7.3
16
+ chromadb==0.5.3
17
+ click==8.2.1
18
+ coloredlogs==15.0.1
19
+ dataclasses-json==0.6.7
20
+ defusedxml==0.7.1
21
+ distro==1.9.0
22
+ dnspython==2.7.0
23
+ durationpy==0.10
24
+ email_validator==2.2.0
25
+ faiss-cpu==1.11.0
26
+ fastapi==0.111.0
27
+ fastapi-cli==0.0.7
28
+ filelock==3.18.0
29
+ frozenlist==1.7.0
30
+ fsspec==2025.5.1
31
+ # Google API Client (YouTube Data API v3 사용을 위해 필요)
32
+ google-api-python-client==2.138.0 # 현재 최신 안정 버전 (확인 필요)
33
+ google-auth==2.40.3
34
+ googleapis-common-protos==1.70.0
35
+ grpcio==1.73.0
36
+ greenlet==3.2.3
37
+ h11==0.16.0
38
+ hf-xet==1.1.3
39
+ httpcore==1.0.9
40
+ httptools==0.6.4
41
+ httpx==0.28.1
42
+ huggingface-hub==0.33.0
43
+ humanfriendly==10.0
44
+ idna==3.10
45
+ importlib_metadata==8.7.0
46
+ importlib_resources==6.5.2
47
+ Jinja2==3.1.6
48
+ joblib==1.5.1
49
+ jsonpatch==1.33
50
+ jsonpointer==3.0.0
51
+ kubernetes==33.1.0
52
+ langchain==0.2.5
53
+ langchain-community==0.2.5
54
+ langchain-core==0.2.43
55
+ langchain-text-splitters==0.2.4
56
+ langsmith==0.1.147
57
+ loguru==0.7.3
58
+ markdown-it-py==3.0.0
59
+ MarkupSafe==3.0.2
60
+ marshmallow==3.26.1
61
+ mdurl==0.1.2
62
+ mmh3==5.1.0
63
+ mpmath==1.3.0
64
+ multidict==6.4.4
65
+ mypy_extensions==1.1.0
66
+ networkx==3.5
67
+ numpy==1.26.4
68
+ oauthlib==3.2.2
69
+ onnxruntime==1.22.0
70
+ opentelemetry-api==1.34.1
71
+ opentelemetry-exporter-otlp-proto-common==1.34.1
72
+ opentelemetry-exporter-otlp-proto-grpc==1.34.1
73
+ opentelemetry-instrumentation==0.55b1
74
+ opentelemetry-instrumentation-asgi==0.55b1
75
+ opentelemetry-instrumentation-fastapi==0.55b1
76
+ opentelemetry-proto==1.34.1
77
+ opentelemetry-sdk==1.34.1
78
+ opentelemetry-semantic-conventions==0.55b1
79
+ opentelemetry-util-http==0.55b1
80
+ orjson==3.10.18
81
+ overrides==7.7.0
82
+ packaging==24.2
83
+ pillow==11.2.1
84
+ posthog==5.0.0
85
+ propcache==0.3.2
86
+ protobuf==5.29.5
87
+ psutil==7.0.0
88
+ pyasn1==0.6.1
89
+ pyasn1_modules==0.4.2
90
+ pydantic==2.11.7
91
+ pydantic_core==2.33.2
92
+ Pygments==2.19.1
93
+ PyPika==0.48.9
94
+ pyproject_hooks==1.2.0
95
+ python-dateutil==2.9.0.post0
96
+ python-dotenv==1.0.1
97
+ python-multipart==0.0.20
98
+ PyYAML==6.0.2
99
+ regex==2024.11.6
100
+ requests==2.32.4
101
+ requests-oauthlib==2.0.0
102
+ requests-toolbelt==1.0.0
103
+ rich==14.0.0
104
+ rich-toolkit==0.14.7
105
+ rsa==4.9.1
106
+ safetensors==0.5.3
107
+ scikit-learn==1.7.0
108
+ scipy==1.15.3
109
+ sentence-transformers==2.7.0
110
+ shellingham==1.5.4
111
+ six==1.17.0
112
+ sniffio==1.3.1
113
+ SQLAlchemy==2.0.41
114
+ starlette==0.37.2
115
+ sympy==1.14.0
116
+ tenacity==8.5.0
117
+ threadpoolctl==3.6.0
118
+ tokenizers==0.19.1
119
+ torch==2.3.1
120
+ tqdm==4.67.1
121
+ transformers==4.41.2
122
+ typer==0.16.0
123
+ typing-inspect==0.9.0
124
+ typing-inspection==0.4.1
125
+ typing_extensions==4.14.0
126
+ ujson==5.10.0
127
+ urllib3==2.4.0
128
+ uvicorn==0.30.1
129
+ uvloop==0.21.0
130
+ watchfiles==1.1.0
131
+ websocket-client==1.8.0
132
+ websockets==15.0.1
133
+ wrapt==1.17.2
134
+ yarl==1.20.1
135
+ yt-dlp==2025.6.9
136
+ zipp==3.23.0
frontend/.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /dist
4
+
5
+
6
+ # local env files
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # Log files
11
+ npm-debug.log*
12
+ yarn-debug.log*
13
+ yarn-error.log*
14
+ pnpm-debug.log*
15
+
16
+ # Editor directories and files
17
+ .idea
18
+ .vscode
19
+ *.suo
20
+ *.ntvs*
21
+ *.njsproj
22
+ *.sln
23
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # frontend
2
+
3
+ ## Project setup
4
+ ```
5
+ npm install
6
+ ```
7
+
8
+ ### Compiles and hot-reloads for development
9
+ ```
10
+ npm run serve
11
+ ```
12
+
13
+ ### Compiles and minifies for production
14
+ ```
15
+ npm run build
16
+ ```
17
+
18
+ ### Lints and fixes files
19
+ ```
20
+ npm run lint
21
+ ```
22
+
23
+ ### Customize configuration
24
+ See [Configuration Reference](https://cli.vuejs.org/config/).
frontend/babel.config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ module.exports = {
2
+ presets: [
3
+ '@vue/cli-plugin-babel/preset'
4
+ ]
5
+ }
frontend/jsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "module": "esnext",
5
+ "baseUrl": "./",
6
+ "moduleResolution": "node",
7
+ "paths": {
8
+ "@/*": [
9
+ "src/*"
10
+ ]
11
+ },
12
+ "lib": [
13
+ "esnext",
14
+ "dom",
15
+ "dom.iterable",
16
+ "scripthost"
17
+ ]
18
+ }
19
+ }
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "serve": "vue-cli-service serve",
7
+ "build": "vue-cli-service build",
8
+ "lint": "vue-cli-service lint"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.10.0",
12
+ "core-js": "^3.8.3",
13
+ "vue": "^3.2.13"
14
+ },
15
+ "devDependencies": {
16
+ "@babel/core": "^7.12.16",
17
+ "@babel/eslint-parser": "^7.12.16",
18
+ "@vue/cli-plugin-babel": "~5.0.0",
19
+ "@vue/cli-plugin-eslint": "~5.0.0",
20
+ "@vue/cli-service": "~5.0.0",
21
+ "eslint": "^7.32.0",
22
+ "eslint-plugin-vue": "^8.0.3"
23
+ },
24
+ "eslintConfig": {
25
+ "root": true,
26
+ "env": {
27
+ "node": true
28
+ },
29
+ "extends": [
30
+ "plugin:vue/vue3-essential",
31
+ "eslint:recommended"
32
+ ],
33
+ "parserOptions": {
34
+ "parser": "@babel/eslint-parser"
35
+ },
36
+ "rules": {}
37
+ },
38
+ "browserslist": [
39
+ "> 1%",
40
+ "last 2 versions",
41
+ "not dead",
42
+ "not ie 11"
43
+ ]
44
+ }
frontend/public/favicon.ico ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
7
+ <link rel="icon" href="<%= BASE_URL %>favicon.ico">
8
+ <title><%= htmlWebpackPlugin.options.title %></title>
9
+ </head>
10
+ <body>
11
+ <noscript>
12
+ <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
13
+ </noscript>
14
+ <div id="app"></div>
15
+ <!-- built files will be auto injected -->
16
+ </body>
17
+ </html>
frontend/src/App.vue ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div id="app">
3
+ <h1>YouTube RAG Explorer</h1>
4
+ <p>영상 URL과 찾고 싶은 내용을 입력해주세요.</p>
5
+
6
+ <div class="input-section">
7
+ <label for="videoUrl">YouTube 영상 URL:</label>
8
+ <input type="text" id="videoUrl" v-model="videoUrl" placeholder="예: https://www.youtube.com/watch?v=..." />
9
+ </div>
10
+
11
+ <div class="input-section">
12
+ <label for="query">찾을 내용 (쿼리):</label>
13
+ <input type="text" id="query" v-model="query" placeholder="예: RAG 기술의 장점은?" />
14
+ </div>
15
+
16
+ <button @click="processVideo" :disabled="loading">
17
+ {{ loading ? '처리 중...' : '영상 탐색 시작' }}
18
+ </button>
19
+
20
+ <div v-if="errorMessage" class="error-message">
21
+ {{ errorMessage }}
22
+ </div>
23
+
24
+ <div v-if="results.length > 0 && !loading" class="results-section">
25
+ <h2>검색 결과:</h2>
26
+ <div v-for="(result, index) in results" :key="index" class="result-item">
27
+ <p><strong>시간:</strong> <a :href="videoUrl + '&t=' + result.timestamp.replace(/:/g, 'm') + 's'" target="_blank">{{ result.timestamp }}</a></p>
28
+ <p><strong>내용:</strong> {{ result.text }}</p>
29
+ </div>
30
+ </div>
31
+ <div v-else-if="!loading && responseMessage" class="info-message">
32
+ {{ responseMessage }}
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ import apiClient from './api'; // axios 인스턴스 import
39
+
40
+ export default {
41
+ name: 'App',
42
+ data() {
43
+ return {
44
+ videoUrl: '',
45
+ query: '',
46
+ loading: false,
47
+ results: [],
48
+ errorMessage: '',
49
+ responseMessage: ''
50
+ };
51
+ },
52
+ methods: {
53
+ async processVideo() {
54
+ this.errorMessage = '';
55
+ this.results = [];
56
+ this.responseMessage = '';
57
+ this.loading = true;
58
+
59
+ try {
60
+ const response = await apiClient.post(`/process_youtube_video`, {
61
+ video_url: this.videoUrl,
62
+ query: this.query
63
+ });
64
+
65
+ this.responseMessage = response.data.message;
66
+ if (response.data.results && response.data.results.length > 0) {
67
+ this.results = response.data.results;
68
+ } else {
69
+ this.responseMessage = response.data.message || "결과를 찾을 수 없습니다.";
70
+ }
71
+
72
+
73
+ } catch (error) {
74
+ console.error("API 호출 중 오류 발생:", error);
75
+ if (error.response) {
76
+ this.errorMessage = `오류: ${error.response.data.detail || error.response.statusText}`;
77
+ } else if (error.request) {
78
+ this.errorMessage = '서버 응답이 없습니다. 백엔드가 실행 중인지 확인하세요.';
79
+ } else {
80
+ this.errorMessage = '요청 설정 중 오류가 발생했습니다.';
81
+ }
82
+ } finally {
83
+ this.loading = false;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ </script>
89
+
90
+ <style>
91
+ #app {
92
+ font-family: Avenir, Helvetica, Arial, sans-serif;
93
+ -webkit-font-smoothing: antialiased;
94
+ -moz-osx-font-smoothing: grayscale;
95
+ text-align: center;
96
+ color: #2c3e50;
97
+ margin-top: 60px;
98
+ max-width: 800px;
99
+ margin-left: auto;
100
+ margin-right: auto;
101
+ padding: 20px;
102
+ border: 1px solid #eee;
103
+ border-radius: 8px;
104
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
105
+ }
106
+
107
+ .input-section {
108
+ margin-bottom: 20px;
109
+ }
110
+
111
+ label {
112
+ display: block;
113
+ font-weight: bold;
114
+ margin-bottom: 5px;
115
+ }
116
+
117
+ input[type="text"] {
118
+ width: calc(100% - 20px);
119
+ padding: 10px;
120
+ border: 1px solid #ccc;
121
+ border-radius: 4px;
122
+ font-size: 16px;
123
+ }
124
+
125
+ button {
126
+ background-color: #42b983;
127
+ color: white;
128
+ padding: 10px 20px;
129
+ border: none;
130
+ border-radius: 4px;
131
+ cursor: pointer;
132
+ font-size: 16px;
133
+ transition: background-color 0.3s ease;
134
+ }
135
+
136
+ button:hover:not(:disabled) {
137
+ background-color: #368a68;
138
+ }
139
+
140
+ button:disabled {
141
+ background-color: #cccccc;
142
+ cursor: not-allowed;
143
+ }
144
+
145
+ .error-message {
146
+ color: red;
147
+ margin-top: 20px;
148
+ font-weight: bold;
149
+ }
150
+
151
+ .info-message {
152
+ color: #3498db;
153
+ margin-top: 20px;
154
+ font-style: italic;
155
+ }
156
+
157
+ .results-section {
158
+ margin-top: 30px;
159
+ text-align: left;
160
+ border-top: 1px solid #eee;
161
+ padding-top: 20px;
162
+ }
163
+
164
+ .results-section h2 {
165
+ text-align: center;
166
+ color: #333;
167
+ }
168
+
169
+ .result-item {
170
+ background-color: #f9f9f9;
171
+ border: 1px solid #ddd;
172
+ border-radius: 6px;
173
+ padding: 15px;
174
+ margin-bottom: 15px;
175
+ }
176
+
177
+ .result-item p {
178
+ margin: 5px 0;
179
+ }
180
+
181
+ .result-item a {
182
+ color: #42b983;
183
+ text-decoration: none;
184
+ }
185
+
186
+ .result-item a:hover {
187
+ text-decoration: underline;
188
+ }
189
+ </style>
frontend/src/api.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ const apiClient = axios.create({
4
+ baseURL: process.env.VUE_APP_API_BASE_URL || '/api',
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ }
8
+ });
9
+
10
+ export default apiClient;
frontend/src/assets/logo.png ADDED
frontend/src/components/HelloWorld.vue ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="hello">
3
+ <h1>{{ msg }}</h1>
4
+ <p>
5
+ For a guide and recipes on how to configure / customize this project,<br>
6
+ check out the
7
+ <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
8
+ </p>
9
+ <h3>Installed CLI Plugins</h3>
10
+ <ul>
11
+ <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
12
+ <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
13
+ </ul>
14
+ <h3>Essential Links</h3>
15
+ <ul>
16
+ <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
17
+ <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
18
+ <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
19
+ <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
20
+ <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
21
+ </ul>
22
+ <h3>Ecosystem</h3>
23
+ <ul>
24
+ <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
25
+ <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
26
+ <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
27
+ <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
28
+ <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
29
+ </ul>
30
+ </div>
31
+ </template>
32
+
33
+ <script>
34
+ export default {
35
+ name: 'HelloWorld',
36
+ props: {
37
+ msg: String
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <!-- Add "scoped" attribute to limit CSS to this component only -->
43
+ <style scoped>
44
+ h3 {
45
+ margin: 40px 0 0;
46
+ }
47
+ ul {
48
+ list-style-type: none;
49
+ padding: 0;
50
+ }
51
+ li {
52
+ display: inline-block;
53
+ margin: 0 10px;
54
+ }
55
+ a {
56
+ color: #42b983;
57
+ }
58
+ </style>
frontend/src/main.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import api from './api'
4
+
5
+ const app = createApp(App)
6
+ app.config.globalProperties.$api = api
7
+ app.mount('#app')
frontend/vue.config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const { defineConfig } = require('@vue/cli-service')
2
+ module.exports = defineConfig({
3
+ transpileDependencies: true,
4
+ publicPath: '/'
5
+ })
get-docker.sh ADDED
@@ -0,0 +1,694 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+ # Docker Engine for Linux installation script.
4
+ #
5
+ # This script is intended as a convenient way to configure docker's package
6
+ # repositories and to install Docker Engine, This script is not recommended
7
+ # for production environments. Before running this script, make yourself familiar
8
+ # with potential risks and limitations, and refer to the installation manual
9
+ # at https://docs.docker.com/engine/install/ for alternative installation methods.
10
+ #
11
+ # The script:
12
+ #
13
+ # - Requires `root` or `sudo` privileges to run.
14
+ # - Attempts to detect your Linux distribution and version and configure your
15
+ # package management system for you.
16
+ # - Doesn't allow you to customize most installation parameters.
17
+ # - Installs dependencies and recommendations without asking for confirmation.
18
+ # - Installs the latest stable release (by default) of Docker CLI, Docker Engine,
19
+ # Docker Buildx, Docker Compose, containerd, and runc. When using this script
20
+ # to provision a machine, this may result in unexpected major version upgrades
21
+ # of these packages. Always test upgrades in a test environment before
22
+ # deploying to your production systems.
23
+ # - Isn't designed to upgrade an existing Docker installation. When using the
24
+ # script to update an existing installation, dependencies may not be updated
25
+ # to the expected version, resulting in outdated versions.
26
+ #
27
+ # Source code is available at https://github.com/docker/docker-install/
28
+ #
29
+ # Usage
30
+ # ==============================================================================
31
+ #
32
+ # To install the latest stable versions of Docker CLI, Docker Engine, and their
33
+ # dependencies:
34
+ #
35
+ # 1. download the script
36
+ #
37
+ # $ curl -fsSL https://get.docker.com -o install-docker.sh
38
+ #
39
+ # 2. verify the script's content
40
+ #
41
+ # $ cat install-docker.sh
42
+ #
43
+ # 3. run the script with --dry-run to verify the steps it executes
44
+ #
45
+ # $ sh install-docker.sh --dry-run
46
+ #
47
+ # 4. run the script either as root, or using sudo to perform the installation.
48
+ #
49
+ # $ sudo sh install-docker.sh
50
+ #
51
+ # Command-line options
52
+ # ==============================================================================
53
+ #
54
+ # --version <VERSION>
55
+ # Use the --version option to install a specific version, for example:
56
+ #
57
+ # $ sudo sh install-docker.sh --version 23.0
58
+ #
59
+ # --channel <stable|test>
60
+ #
61
+ # Use the --channel option to install from an alternative installation channel.
62
+ # The following example installs the latest versions from the "test" channel,
63
+ # which includes pre-releases (alpha, beta, rc):
64
+ #
65
+ # $ sudo sh install-docker.sh --channel test
66
+ #
67
+ # Alternatively, use the script at https://test.docker.com, which uses the test
68
+ # channel as default.
69
+ #
70
+ # --mirror <Aliyun|AzureChinaCloud>
71
+ #
72
+ # Use the --mirror option to install from a mirror supported by this script.
73
+ # Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and
74
+ # "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example:
75
+ #
76
+ # $ sudo sh install-docker.sh --mirror AzureChinaCloud
77
+ #
78
+ # ==============================================================================
79
+
80
+
81
+ # Git commit from https://github.com/docker/docker-install when
82
+ # the script was uploaded (Should only be modified by upload job):
83
+ SCRIPT_COMMIT_SHA="53a22f61c0628e58e1d6680b49e82993d304b449"
84
+
85
+ # strip "v" prefix if present
86
+ VERSION="${VERSION#v}"
87
+
88
+ # The channel to install from:
89
+ # * stable
90
+ # * test
91
+ DEFAULT_CHANNEL_VALUE="stable"
92
+ if [ -z "$CHANNEL" ]; then
93
+ CHANNEL=$DEFAULT_CHANNEL_VALUE
94
+ fi
95
+
96
+ DEFAULT_DOWNLOAD_URL="https://download.docker.com"
97
+ if [ -z "$DOWNLOAD_URL" ]; then
98
+ DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL
99
+ fi
100
+
101
+ DEFAULT_REPO_FILE="docker-ce.repo"
102
+ if [ -z "$REPO_FILE" ]; then
103
+ REPO_FILE="$DEFAULT_REPO_FILE"
104
+ # Automatically default to a staging repo fora
105
+ # a staging download url (download-stage.docker.com)
106
+ case "$DOWNLOAD_URL" in
107
+ *-stage*) REPO_FILE="docker-ce-staging.repo";;
108
+ esac
109
+ fi
110
+
111
+ mirror=''
112
+ DRY_RUN=${DRY_RUN:-}
113
+ while [ $# -gt 0 ]; do
114
+ case "$1" in
115
+ --channel)
116
+ CHANNEL="$2"
117
+ shift
118
+ ;;
119
+ --dry-run)
120
+ DRY_RUN=1
121
+ ;;
122
+ --mirror)
123
+ mirror="$2"
124
+ shift
125
+ ;;
126
+ --version)
127
+ VERSION="${2#v}"
128
+ shift
129
+ ;;
130
+ --*)
131
+ echo "Illegal option $1"
132
+ ;;
133
+ esac
134
+ shift $(( $# > 0 ? 1 : 0 ))
135
+ done
136
+
137
+ case "$mirror" in
138
+ Aliyun)
139
+ DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce"
140
+ ;;
141
+ AzureChinaCloud)
142
+ DOWNLOAD_URL="https://mirror.azure.cn/docker-ce"
143
+ ;;
144
+ "")
145
+ ;;
146
+ *)
147
+ >&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'."
148
+ exit 1
149
+ ;;
150
+ esac
151
+
152
+ case "$CHANNEL" in
153
+ stable|test)
154
+ ;;
155
+ *)
156
+ >&2 echo "unknown CHANNEL '$CHANNEL': use either stable or test."
157
+ exit 1
158
+ ;;
159
+ esac
160
+
161
+ command_exists() {
162
+ command -v "$@" > /dev/null 2>&1
163
+ }
164
+
165
+ # version_gte checks if the version specified in $VERSION is at least the given
166
+ # SemVer (Maj.Minor[.Patch]), or CalVer (YY.MM) version.It returns 0 (success)
167
+ # if $VERSION is either unset (=latest) or newer or equal than the specified
168
+ # version, or returns 1 (fail) otherwise.
169
+ #
170
+ # examples:
171
+ #
172
+ # VERSION=23.0
173
+ # version_gte 23.0 // 0 (success)
174
+ # version_gte 20.10 // 0 (success)
175
+ # version_gte 19.03 // 0 (success)
176
+ # version_gte 26.1 // 1 (fail)
177
+ version_gte() {
178
+ if [ -z "$VERSION" ]; then
179
+ return 0
180
+ fi
181
+ version_compare "$VERSION" "$1"
182
+ }
183
+
184
+ # version_compare compares two version strings (either SemVer (Major.Minor.Path),
185
+ # or CalVer (YY.MM) version strings. It returns 0 (success) if version A is newer
186
+ # or equal than version B, or 1 (fail) otherwise. Patch releases and pre-release
187
+ # (-alpha/-beta) are not taken into account
188
+ #
189
+ # examples:
190
+ #
191
+ # version_compare 23.0.0 20.10 // 0 (success)
192
+ # version_compare 23.0 20.10 // 0 (success)
193
+ # version_compare 20.10 19.03 // 0 (success)
194
+ # version_compare 20.10 20.10 // 0 (success)
195
+ # version_compare 19.03 20.10 // 1 (fail)
196
+ version_compare() (
197
+ set +x
198
+
199
+ yy_a="$(echo "$1" | cut -d'.' -f1)"
200
+ yy_b="$(echo "$2" | cut -d'.' -f1)"
201
+ if [ "$yy_a" -lt "$yy_b" ]; then
202
+ return 1
203
+ fi
204
+ if [ "$yy_a" -gt "$yy_b" ]; then
205
+ return 0
206
+ fi
207
+ mm_a="$(echo "$1" | cut -d'.' -f2)"
208
+ mm_b="$(echo "$2" | cut -d'.' -f2)"
209
+
210
+ # trim leading zeros to accommodate CalVer
211
+ mm_a="${mm_a#0}"
212
+ mm_b="${mm_b#0}"
213
+
214
+ if [ "${mm_a:-0}" -lt "${mm_b:-0}" ]; then
215
+ return 1
216
+ fi
217
+
218
+ return 0
219
+ )
220
+
221
+ is_dry_run() {
222
+ if [ -z "$DRY_RUN" ]; then
223
+ return 1
224
+ else
225
+ return 0
226
+ fi
227
+ }
228
+
229
+ is_wsl() {
230
+ case "$(uname -r)" in
231
+ *microsoft* ) true ;; # WSL 2
232
+ *Microsoft* ) true ;; # WSL 1
233
+ * ) false;;
234
+ esac
235
+ }
236
+
237
+ is_darwin() {
238
+ case "$(uname -s)" in
239
+ *darwin* ) true ;;
240
+ *Darwin* ) true ;;
241
+ * ) false;;
242
+ esac
243
+ }
244
+
245
+ deprecation_notice() {
246
+ distro=$1
247
+ distro_version=$2
248
+ echo
249
+ printf "\033[91;1mDEPRECATION WARNING\033[0m\n"
250
+ printf " This Linux distribution (\033[1m%s %s\033[0m) reached end-of-life and is no longer supported by this script.\n" "$distro" "$distro_version"
251
+ echo " No updates or security fixes will be released for this distribution, and users are recommended"
252
+ echo " to upgrade to a currently maintained version of $distro."
253
+ echo
254
+ printf "Press \033[1mCtrl+C\033[0m now to abort this script, or wait for the installation to continue."
255
+ echo
256
+ sleep 10
257
+ }
258
+
259
+ get_distribution() {
260
+ lsb_dist=""
261
+ # Every system that we officially support has /etc/os-release
262
+ if [ -r /etc/os-release ]; then
263
+ lsb_dist="$(. /etc/os-release && echo "$ID")"
264
+ fi
265
+ # Returning an empty string here should be alright since the
266
+ # case statements don't act unless you provide an actual value
267
+ echo "$lsb_dist"
268
+ }
269
+
270
+ echo_docker_as_nonroot() {
271
+ if is_dry_run; then
272
+ return
273
+ fi
274
+ if command_exists docker && [ -e /var/run/docker.sock ]; then
275
+ (
276
+ set -x
277
+ $sh_c 'docker version'
278
+ ) || true
279
+ fi
280
+
281
+ # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output
282
+ echo
283
+ echo "================================================================================"
284
+ echo
285
+ if version_gte "20.10"; then
286
+ echo "To run Docker as a non-privileged user, consider setting up the"
287
+ echo "Docker daemon in rootless mode for your user:"
288
+ echo
289
+ echo " dockerd-rootless-setuptool.sh install"
290
+ echo
291
+ echo "Visit https://docs.docker.com/go/rootless/ to learn about rootless mode."
292
+ echo
293
+ fi
294
+ echo
295
+ echo "To run the Docker daemon as a fully privileged service, but granting non-root"
296
+ echo "users access, refer to https://docs.docker.com/go/daemon-access/"
297
+ echo
298
+ echo "WARNING: Access to the remote API on a privileged Docker daemon is equivalent"
299
+ echo " to root access on the host. Refer to the 'Docker daemon attack surface'"
300
+ echo " documentation for details: https://docs.docker.com/go/attack-surface/"
301
+ echo
302
+ echo "================================================================================"
303
+ echo
304
+ }
305
+
306
+ # Check if this is a forked Linux distro
307
+ check_forked() {
308
+
309
+ # Check for lsb_release command existence, it usually exists in forked distros
310
+ if command_exists lsb_release; then
311
+ # Check if the `-u` option is supported
312
+ set +e
313
+ lsb_release -a -u > /dev/null 2>&1
314
+ lsb_release_exit_code=$?
315
+ set -e
316
+
317
+ # Check if the command has exited successfully, it means we're in a forked distro
318
+ if [ "$lsb_release_exit_code" = "0" ]; then
319
+ # Print info about current distro
320
+ cat <<-EOF
321
+ You're using '$lsb_dist' version '$dist_version'.
322
+ EOF
323
+
324
+ # Get the upstream release info
325
+ lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]')
326
+ dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]')
327
+
328
+ # Print info about upstream distro
329
+ cat <<-EOF
330
+ Upstream release is '$lsb_dist' version '$dist_version'.
331
+ EOF
332
+ else
333
+ if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then
334
+ if [ "$lsb_dist" = "osmc" ]; then
335
+ # OSMC runs Raspbian
336
+ lsb_dist=raspbian
337
+ else
338
+ # We're Debian and don't even know it!
339
+ lsb_dist=debian
340
+ fi
341
+ dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
342
+ case "$dist_version" in
343
+ 13)
344
+ dist_version="trixie"
345
+ ;;
346
+ 12)
347
+ dist_version="bookworm"
348
+ ;;
349
+ 11)
350
+ dist_version="bullseye"
351
+ ;;
352
+ 10)
353
+ dist_version="buster"
354
+ ;;
355
+ 9)
356
+ dist_version="stretch"
357
+ ;;
358
+ 8)
359
+ dist_version="jessie"
360
+ ;;
361
+ esac
362
+ fi
363
+ fi
364
+ fi
365
+ }
366
+
367
+ do_install() {
368
+ echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA"
369
+
370
+ if command_exists docker; then
371
+ cat >&2 <<-'EOF'
372
+ Warning: the "docker" command appears to already exist on this system.
373
+
374
+ If you already have Docker installed, this script can cause trouble, which is
375
+ why we're displaying this warning and provide the opportunity to cancel the
376
+ installation.
377
+
378
+ If you installed the current Docker package using this script and are using it
379
+ again to update Docker, you can ignore this message, but be aware that the
380
+ script resets any custom changes in the deb and rpm repo configuration
381
+ files to match the parameters passed to the script.
382
+
383
+ You may press Ctrl+C now to abort this script.
384
+ EOF
385
+ ( set -x; sleep 20 )
386
+ fi
387
+
388
+ user="$(id -un 2>/dev/null || true)"
389
+
390
+ sh_c='sh -c'
391
+ if [ "$user" != 'root' ]; then
392
+ if command_exists sudo; then
393
+ sh_c='sudo -E sh -c'
394
+ elif command_exists su; then
395
+ sh_c='su -c'
396
+ else
397
+ cat >&2 <<-'EOF'
398
+ Error: this installer needs the ability to run commands as root.
399
+ We are unable to find either "sudo" or "su" available to make this happen.
400
+ EOF
401
+ exit 1
402
+ fi
403
+ fi
404
+
405
+ if is_dry_run; then
406
+ sh_c="echo"
407
+ fi
408
+
409
+ # perform some very rudimentary platform detection
410
+ lsb_dist=$( get_distribution )
411
+ lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')"
412
+
413
+ if is_wsl; then
414
+ echo
415
+ echo "WSL DETECTED: We recommend using Docker Desktop for Windows."
416
+ echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop/"
417
+ echo
418
+ cat >&2 <<-'EOF'
419
+
420
+ You may press Ctrl+C now to abort this script.
421
+ EOF
422
+ ( set -x; sleep 20 )
423
+ fi
424
+
425
+ case "$lsb_dist" in
426
+
427
+ ubuntu)
428
+ if command_exists lsb_release; then
429
+ dist_version="$(lsb_release --codename | cut -f2)"
430
+ fi
431
+ if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then
432
+ dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")"
433
+ fi
434
+ ;;
435
+
436
+ debian|raspbian)
437
+ dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
438
+ case "$dist_version" in
439
+ 13)
440
+ dist_version="trixie"
441
+ ;;
442
+ 12)
443
+ dist_version="bookworm"
444
+ ;;
445
+ 11)
446
+ dist_version="bullseye"
447
+ ;;
448
+ 10)
449
+ dist_version="buster"
450
+ ;;
451
+ 9)
452
+ dist_version="stretch"
453
+ ;;
454
+ 8)
455
+ dist_version="jessie"
456
+ ;;
457
+ esac
458
+ ;;
459
+
460
+ centos|rhel)
461
+ if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
462
+ dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
463
+ fi
464
+ ;;
465
+
466
+ *)
467
+ if command_exists lsb_release; then
468
+ dist_version="$(lsb_release --release | cut -f2)"
469
+ fi
470
+ if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then
471
+ dist_version="$(. /etc/os-release && echo "$VERSION_ID")"
472
+ fi
473
+ ;;
474
+
475
+ esac
476
+
477
+ # Check if this is a forked Linux distro
478
+ check_forked
479
+
480
+ # Print deprecation warnings for distro versions that recently reached EOL,
481
+ # but may still be commonly used (especially LTS versions).
482
+ case "$lsb_dist.$dist_version" in
483
+ centos.8|centos.7|rhel.7)
484
+ deprecation_notice "$lsb_dist" "$dist_version"
485
+ ;;
486
+ debian.buster|debian.stretch|debian.jessie)
487
+ deprecation_notice "$lsb_dist" "$dist_version"
488
+ ;;
489
+ raspbian.buster|raspbian.stretch|raspbian.jessie)
490
+ deprecation_notice "$lsb_dist" "$dist_version"
491
+ ;;
492
+ ubuntu.bionic|ubuntu.xenial|ubuntu.trusty)
493
+ deprecation_notice "$lsb_dist" "$dist_version"
494
+ ;;
495
+ ubuntu.mantic|ubuntu.lunar|ubuntu.kinetic|ubuntu.impish|ubuntu.hirsute|ubuntu.groovy|ubuntu.eoan|ubuntu.disco|ubuntu.cosmic)
496
+ deprecation_notice "$lsb_dist" "$dist_version"
497
+ ;;
498
+ fedora.*)
499
+ if [ "$dist_version" -lt 40 ]; then
500
+ deprecation_notice "$lsb_dist" "$dist_version"
501
+ fi
502
+ ;;
503
+ esac
504
+
505
+ # Run setup for each distro accordingly
506
+ case "$lsb_dist" in
507
+ ubuntu|debian|raspbian)
508
+ pre_reqs="ca-certificates curl"
509
+ apt_repo="deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL"
510
+ (
511
+ if ! is_dry_run; then
512
+ set -x
513
+ fi
514
+ $sh_c 'apt-get -qq update >/dev/null'
515
+ $sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pre_reqs >/dev/null"
516
+ $sh_c 'install -m 0755 -d /etc/apt/keyrings'
517
+ $sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" -o /etc/apt/keyrings/docker.asc"
518
+ $sh_c "chmod a+r /etc/apt/keyrings/docker.asc"
519
+ $sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list"
520
+ $sh_c 'apt-get -qq update >/dev/null'
521
+ )
522
+ pkg_version=""
523
+ if [ -n "$VERSION" ]; then
524
+ if is_dry_run; then
525
+ echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
526
+ else
527
+ # Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel
528
+ pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')"
529
+ search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
530
+ pkg_version="$($sh_c "$search_command")"
531
+ echo "INFO: Searching repository for VERSION '$VERSION'"
532
+ echo "INFO: $search_command"
533
+ if [ -z "$pkg_version" ]; then
534
+ echo
535
+ echo "ERROR: '$VERSION' not found amongst apt-cache madison results"
536
+ echo
537
+ exit 1
538
+ fi
539
+ if version_gte "18.09"; then
540
+ search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
541
+ echo "INFO: $search_command"
542
+ cli_pkg_version="=$($sh_c "$search_command")"
543
+ fi
544
+ pkg_version="=$pkg_version"
545
+ fi
546
+ fi
547
+ (
548
+ pkgs="docker-ce${pkg_version%=}"
549
+ if version_gte "18.09"; then
550
+ # older versions didn't ship the cli and containerd as separate packages
551
+ pkgs="$pkgs docker-ce-cli${cli_pkg_version%=} containerd.io"
552
+ fi
553
+ if version_gte "20.10"; then
554
+ pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
555
+ fi
556
+ if version_gte "23.0"; then
557
+ pkgs="$pkgs docker-buildx-plugin"
558
+ fi
559
+ if ! is_dry_run; then
560
+ set -x
561
+ fi
562
+ $sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pkgs >/dev/null"
563
+ )
564
+ echo_docker_as_nonroot
565
+ exit 0
566
+ ;;
567
+ centos|fedora|rhel)
568
+ if [ "$(uname -m)" = "s390x" ]; then
569
+ echo "Effective v27.5, please consult RHEL distro statement for s390x support."
570
+ exit 1
571
+ fi
572
+ repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE"
573
+ (
574
+ if ! is_dry_run; then
575
+ set -x
576
+ fi
577
+ if command_exists dnf5; then
578
+ $sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
579
+ $sh_c "dnf5 config-manager addrepo --overwrite --save-filename=docker-ce.repo --from-repofile='$repo_file_url'"
580
+
581
+ if [ "$CHANNEL" != "stable" ]; then
582
+ $sh_c "dnf5 config-manager setopt \"docker-ce-*.enabled=0\""
583
+ $sh_c "dnf5 config-manager setopt \"docker-ce-$CHANNEL.enabled=1\""
584
+ fi
585
+ $sh_c "dnf makecache"
586
+ elif command_exists dnf; then
587
+ $sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core"
588
+ $sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
589
+ $sh_c "dnf config-manager --add-repo $repo_file_url"
590
+
591
+ if [ "$CHANNEL" != "stable" ]; then
592
+ $sh_c "dnf config-manager --set-disabled \"docker-ce-*\""
593
+ $sh_c "dnf config-manager --set-enabled \"docker-ce-$CHANNEL\""
594
+ fi
595
+ $sh_c "dnf makecache"
596
+ else
597
+ $sh_c "yum -y -q install yum-utils"
598
+ $sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo"
599
+ $sh_c "yum-config-manager --add-repo $repo_file_url"
600
+
601
+ if [ "$CHANNEL" != "stable" ]; then
602
+ $sh_c "yum-config-manager --disable \"docker-ce-*\""
603
+ $sh_c "yum-config-manager --enable \"docker-ce-$CHANNEL\""
604
+ fi
605
+ $sh_c "yum makecache"
606
+ fi
607
+ )
608
+ pkg_version=""
609
+ if command_exists dnf; then
610
+ pkg_manager="dnf"
611
+ pkg_manager_flags="-y -q --best"
612
+ else
613
+ pkg_manager="yum"
614
+ pkg_manager_flags="-y -q"
615
+ fi
616
+ if [ -n "$VERSION" ]; then
617
+ if is_dry_run; then
618
+ echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
619
+ else
620
+ if [ "$lsb_dist" = "fedora" ]; then
621
+ pkg_suffix="fc$dist_version"
622
+ else
623
+ pkg_suffix="el"
624
+ fi
625
+ pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g').*$pkg_suffix"
626
+ search_command="$pkg_manager list --showduplicates docker-ce | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
627
+ pkg_version="$($sh_c "$search_command")"
628
+ echo "INFO: Searching repository for VERSION '$VERSION'"
629
+ echo "INFO: $search_command"
630
+ if [ -z "$pkg_version" ]; then
631
+ echo
632
+ echo "ERROR: '$VERSION' not found amongst $pkg_manager list results"
633
+ echo
634
+ exit 1
635
+ fi
636
+ if version_gte "18.09"; then
637
+ # older versions don't support a cli package
638
+ search_command="$pkg_manager list --showduplicates docker-ce-cli | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'"
639
+ cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)"
640
+ fi
641
+ # Cut out the epoch and prefix with a '-'
642
+ pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)"
643
+ fi
644
+ fi
645
+ (
646
+ pkgs="docker-ce$pkg_version"
647
+ if version_gte "18.09"; then
648
+ # older versions didn't ship the cli and containerd as separate packages
649
+ if [ -n "$cli_pkg_version" ]; then
650
+ pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io"
651
+ else
652
+ pkgs="$pkgs docker-ce-cli containerd.io"
653
+ fi
654
+ fi
655
+ if version_gte "20.10"; then
656
+ pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version"
657
+ fi
658
+ if version_gte "23.0"; then
659
+ pkgs="$pkgs docker-buildx-plugin"
660
+ fi
661
+ if ! is_dry_run; then
662
+ set -x
663
+ fi
664
+ $sh_c "$pkg_manager $pkg_manager_flags install $pkgs"
665
+ )
666
+ echo_docker_as_nonroot
667
+ exit 0
668
+ ;;
669
+ sles)
670
+ echo "Effective v27.5, please consult SLES distro statement for s390x support."
671
+ exit 1
672
+ ;;
673
+ *)
674
+ if [ -z "$lsb_dist" ]; then
675
+ if is_darwin; then
676
+ echo
677
+ echo "ERROR: Unsupported operating system 'macOS'"
678
+ echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop"
679
+ echo
680
+ exit 1
681
+ fi
682
+ fi
683
+ echo
684
+ echo "ERROR: Unsupported distribution '$lsb_dist'"
685
+ echo
686
+ exit 1
687
+ ;;
688
+ esac
689
+ exit 1
690
+ }
691
+
692
+ # wrapped up in a function so that we have some protection against only getting
693
+ # half the file during "curl | sh"
694
+ do_install
nginx.conf ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # nginx.conf
2
+
3
+ worker_processes auto;
4
+ events {
5
+ worker_connections 1024;
6
+ }
7
+
8
+ http {
9
+ include mime.types;
10
+ default_type application/octet-stream;
11
+ sendfile on;
12
+ keepalive_timeout 65;
13
+
14
+ server {
15
+ listen 8000; # 컨테이너 내부에서 8000번 포트로 수신
16
+
17
+ # Vue.js 정적 파일 서빙
18
+ location / {
19
+ root /usr/share/nginx/html; # Vue.js 빌드 결과물이 복사될 경로
20
+ index index.html index.htm;
21
+ try_files $uri $uri/ /index.html; # Vue Router history mode 지원
22
+ }
23
+
24
+ # FastAPI 백엔드 API로 프록시 (FastAPI는 컨테이너 내부에서 8000번 포트로 실행됩니다)
25
+ # Nginx가 FastAPI 뒤에 있으므로, FastAPI가 8000번 포트로 들어오는 요청을 받습니다.
26
+ # Vue.js에서 /api/v1/process_youtube_video 로 요청하면, 백엔드로 전달됩니다.
27
+ # 주의: 여기서는 간단하게 /api 경로로 들어오는 모든 요청을 백엔드로 프록시합니다.
28
+ # 실제 앱에서는 FastAPI가 동일한 8000번 포트에서 Nginx 없이 직접 실행되거나,
29
+ # Nginx가 다른 포트로 들어오는 요청을 백엔드 포트(예: 8001)로 프록시하는 형태가 될 수 있습니다.
30
+ # 여기서는 Nginx가 Vue 정적 파일을 서빙하고, 백엔드는 Nginx를 통하지 않고
31
+ # 직접 8000번 포트에서 실행될 것이므로, 이 proxy_pass 부분은 현재 필요 없습니다.
32
+ # (Uvicorn이 직접 8000번 포트를 사용하고 Nginx는 다른 포트를 사용하는 경우의 예시)
33
+ # location /api/ {
34
+ # proxy_pass http://localhost:8000; # FastAPI가 8000번 포트에서 실행 중이라고 가정
35
+ # proxy_set_header Host $host;
36
+ # proxy_set_header X-Real-IP $remote_addr;
37
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38
+ # proxy_set_header X-Forwarded-Proto $scheme;
39
+ # }
40
+
41
+ # 하지만, 허깅페이스 스페이스 Dockerfile 구성은 보통 Uvicorn이 직접 8000번 포트를 리슨합니다.
42
+ # Vue 앱은 빌드되어 /usr/share/nginx/html 에 있고, Nginx는 그 정적 파일만 서빙합니다.
43
+ # API 호출은 Vue 앱에서 직접 Uvicorn으로 이루어집니다 (동일 포트).
44
+ # 따라서 위 proxy_pass 설정은 이 경우 필요 없습니다.
45
+ # Dockerfile에서 Nginx를 사용하지 않고 Uvicorn이 Vue 앱의 정적 파일까지 서빙하는 경우가 더 흔합니다.
46
+
47
+ # --- 수정: 허깅페이스 스페이스 Docker 환경에 맞게 Nginx 역할 최소화 ---
48
+ # 허깅페이스 스페이스는 Docker 컨테이너의 8000번 포트를 외부로 노출합니다.
49
+ # 일반적으로 Uvicorn(FastAPI)이 이 8000번 포트를 직접 리슨하고,
50
+ # Vue.js 빌드 결과물은 Uvicorn의 Starlette StaticFiles 기능을 통해 서빙하거나,
51
+ # Nginx를 별도로 띄워 정적 파일만 서빙하고 API는 직접 Uvicorn으로 보내는 방식입니다.
52
+ # 여기서는 Uvicorn이 8000번 포트를 리슨하고, Nginx는 별도의 포트에서 정적 파일만 서빙하는
53
+ # 복잡한 구성을 피하고, Uvicorn이 모든 것을 처리하도록 구성하는 것이 더 간단합니다.
54
+
55
+ # 잠시 보류하고, 다음 start.sh 스크립트와 함께 설명하겠습니다.
56
+ # Nginx를 사용하지 않는 것이 더 일반적입니다.
57
+
58
+ # 임시로 이 부분은 주석 처리합니다.
59
+ # location /api/ {
60
+ # proxy_pass http://127.0.0.1:8000;
61
+ # proxy_set_header Host $host;
62
+ # proxy_set_header X-Real-IP $remote_addr;
63
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
64
+ # }
65
+ }
66
+ }