Spaces:
Build error
Build error
| <template> | |
| <div id="app" class="main-container"> | |
| <div class="left-panel"> | |
| <h1>YouTube Transcript Extraction</h1> | |
| <p>영상 URL과 찾고 싶은 내용을 입력해주세요.</p> | |
| <div class="input-section"> | |
| <label for="videoUrl">YouTube 영상 URL:</label> | |
| <input type="text" id="videoUrl" v-model="videoUrl" placeholder="예: https://www.youtube.com/watch?v=xxxxxxxxxxx" @input="updateVideoEmbed" /> | |
| </div> | |
| <div v-if="videoEmbedUrl" class="video-embed"> | |
| <iframe | |
| :src="videoEmbedUrl" | |
| width="100%" | |
| height="400" | |
| frameborder="0" | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | |
| allowfullscreen | |
| id="youtube-iframe" ></iframe> | |
| </div> | |
| <div class="input-section"> | |
| <label for="query">찾을 내용 (쿼리):</label> | |
| <input type="text" id="query" v-model="query" placeholder="예: RAG 기술의 장점은?" /> | |
| </div> | |
| <button @click="processVideo" :disabled="loading" class="center-button"> | |
| {{ loading ? '처리 중...' : '영상 탐색 시작' }} | |
| </button> | |
| <div v-if="errorMessage" class="error-message"> | |
| {{ errorMessage }} | |
| </div> | |
| <div v-if="generatedAnswer" class="generated-answer-section"> | |
| <h2>✨ 생성된 답변:</h2> | |
| <p>{{ generatedAnswer }}</p> | |
| </div> | |
| <div v-if="responseMessage && !generatedAnswer && !errorMessage" class="info-message"> | |
| {{ responseMessage }} | |
| </div> | |
| </div> | |
| <div class="right-sidebar"> | |
| <h2>검색 결과 (타임 라인):</h2> | |
| <div v-if="loading && results.length === 0 && !errorMessage"> | |
| <p>검색 결과를 불러오는 중...</p> | |
| </div> | |
| <div v-else-if="results.length > 0"> | |
| <div v-for="(result, index) in results" :key="index" class="result-item"> | |
| <p> | |
| <strong class="timestamp-link" @click="seekVideo(result.timestamp)"> | |
| 시간: {{ result.timestamp }} | |
| </strong> | |
| </p> | |
| <p><strong>내용:</strong> {{ result.text }}</p> | |
| </div> | |
| </div> | |
| <div v-else> | |
| <p>아직 검색 결과가 없습니다.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| export default { | |
| name: 'App', | |
| data() { | |
| return { | |
| videoUrl: '', | |
| query: '', | |
| loading: false, | |
| results: [], | |
| errorMessage: '', | |
| responseMessage: '', | |
| videoEmbedUrl: '', | |
| generatedAnswer: '' | |
| }; | |
| }, | |
| methods: { | |
| updateVideoEmbed() { | |
| this.videoEmbedUrl = ''; | |
| this.errorMessage = ''; | |
| if (!this.videoUrl) return; | |
| const regex = /(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; | |
| const match = this.videoUrl.match(regex); | |
| if (match && match[1]) { | |
| // 유튜브 임베드 URL 수정 (http -> https, googleusercontent.com 제거) | |
| this.videoEmbedUrl = `https://www.youtube.com/embed/${match[1]}`; | |
| } else{ | |
| this.errorMessage = '유효한 유튜브 URL을 입력해주세요.'; | |
| } | |
| }, | |
| async processVideo() { | |
| this.errorMessage = ''; | |
| this.results = []; | |
| this.responseMessage = ''; | |
| this.loading = true; | |
| this.generatedAnswer = ''; | |
| try { | |
| const backendUrl = process.env.VUE_APP_BACKEND_URL || 'http://localhost:7860'; | |
| const response = await fetch(`${backendUrl}/api/process_youtube_video`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| video_url: this.videoUrl, | |
| query: this.query, | |
| }), | |
| }) | |
| const data = await response.json(); | |
| if (response.ok) { | |
| if (data.status === 'success') { | |
| this.results = data.retrieved_chunks || []; | |
| this.generatedAnswer = data.generated_answer || '답변을 생성하지 못했습니다.'; | |
| this.responseMessage = data.message; | |
| if (this.results.length === 0 && !this.generatedAnswer) { | |
| this.errorMessage = '관련된 정보나 답변을 찾을 수 없습니다.'; | |
| } else if (this.results.length > 0 && !this.generatedAnswer) { | |
| this.errorMessage = '검색된 정보를 바탕으로 답변을 생성하지 못했습니다. Ollama 서버를 확인해주세요.'; | |
| } | |
| } else { | |
| this.errorMessage = data.message || '영상을 처리하는 데 실패했습니다.'; | |
| this.results = []; | |
| this.generatedAnswer = ''; | |
| } | |
| } else { | |
| this.errorMessage = data.detail || '서버 오류가 발생했습니다.' | |
| this.results = []; | |
| this.generatedAnswer = ''; | |
| } | |
| } catch (error) { | |
| console.error('Error processing video:', error); | |
| this.errorMessage = '네트워크 오류 또는 서버에 연결할 수 없습니다.'; | |
| this.results = []; | |
| this.generatedAnswer = ''; | |
| } finally { | |
| this.loading = false; | |
| } | |
| }, | |
| // hh:mm:ss 형식을 초 단위로 변환하는 헬퍼 함수 | |
| timeToSeconds(timeString) { | |
| const parts = timeString.split(':').map(Number); | |
| if (parts.length === 3) { // HH:MM:SS | |
| return parts[0] * 3600 + parts[1] * 60 + parts[2]; | |
| } else if (parts.length === 2) { // MM:SS (일부 자막은 이 형식일 수 있음) | |
| return parts[0] * 60 + parts[1]; | |
| } | |
| return 0; // 유효하지 않은 형식은 0으로 처리 | |
| }, | |
| // 비디오를 특정 시간으로 이동시키는 함수 | |
| seekVideo(timestamp) { | |
| const seconds = this.timeToSeconds(timestamp); | |
| const iframe = document.getElementById('youtube-iframe'); | |
| if (iframe && iframe.src) { | |
| const currentSrc = iframe.src; | |
| // 기존 쿼리 파라미터 유지하고 &start=xx 추가 또는 업데이트 | |
| let newSrc; | |
| const urlObj = new URL(currentSrc); | |
| urlObj.searchParams.set('start', seconds); | |
| // autoplay도 추가하여 클릭 시 재생되도록 할 수 있음 | |
| urlObj.searchParams.set('autoplay', '1'); | |
| newSrc = urlObj.toString(); | |
| iframe.src = newSrc; | |
| } else { | |
| this.errorMessage = '비디오 플레이어를 찾을 수 없습니다. URL을 먼저 입력해주세요.'; | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* 기존 #app 스타일을 main-container에 적용 및 수정 */ | |
| .main-container { | |
| font-family: Avenir, Helvetica, Arial, sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| text-align: center; | |
| color: #2c3e50; | |
| margin-top: 60px; | |
| max-width: 1200px; /* 전체 컨테이너 너비 증가 */ | |
| margin-left: auto; | |
| margin-right: auto; | |
| padding: 20px; | |
| border: 1px solid #eee; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| /* Flexbox 레이아웃 설정 */ | |
| display: flex; | |
| flex-direction: row; /* 자식 요소들을 가로로 배치 */ | |
| gap: 30px; /* 왼쪽 패널과 사이드바 사이의 간격 */ | |
| align-items: flex-start; /* 상단 정렬 */ | |
| } | |
| /* 왼쪽 패널 스타일 */ | |
| .left-panel { | |
| flex: 2; /* 왼쪽 패널이 더 많은 공간 차지 (예: 66%) */ | |
| text-align: center; /* 왼쪽 패널 내 텍스트 왼쪽 정렬 */ | |
| padding-right: 15px; /* 사이드바와의 시각적 구분 */ | |
| } | |
| /* 오른쪽 사이드바 스타일 */ | |
| .right-sidebar { | |
| flex: 1; /* 오른쪽 사이드바가 남은 공간 차지 (예: 33%) */ | |
| text-align: left; /* 사이드바 내 텍스트 왼쪽 정렬 */ | |
| padding-left: 15px; /* 왼쪽 패널과의 시각적 구분 */ | |
| border-left: 1px solid #eee; /* 구분선 */ | |
| max-height: 80vh; /* 화면 높이의 80%를 넘지 않도록 */ | |
| overflow-y: auto; /* 내용이 넘치면 세로 스크롤바 생성 */ | |
| padding-bottom: 20px; /* 스크롤 시 하단 여백 */ | |
| } | |
| /* 기존 스타일 유지 (수정된 .main-container, .left-panel, .right-sidebar 제외) */ | |
| h1 { | |
| color: #42b983; | |
| text-align: center; /* 제목 중앙 정렬 유지 */ | |
| } | |
| p { | |
| text-align: center; /* 설명 문구 중앙 정렬 유지 */ | |
| } | |
| .input-section { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| input[type="text"] { | |
| width: calc(100% - 20px); | |
| padding: 10px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| font-size: 16px; | |
| } | |
| button { | |
| background-color: #42b983; | |
| color: white; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background-color 0.3s ease; | |
| margin-top: 10px; /* 버튼 위쪽 여백 추가 */ | |
| } | |
| button:hover:not(:disabled) { | |
| background-color: #368a68; | |
| } | |
| button:disabled { | |
| background-color: #cccccc; | |
| cursor: not-allowed; | |
| } | |
| .error-message { | |
| color: red; | |
| margin-top: 20px; | |
| font-weight: bold; | |
| background-color: #ffebee; | |
| border: 1px solid #ef9a9a; | |
| padding: 10px; | |
| border-radius: 4px; | |
| } | |
| .generated-answer-section { | |
| margin-top: 30px; | |
| padding: 20px; | |
| background-color: #e8f5e9; /* 부드러운 녹색 배경 */ | |
| border-left: 5px solid #4CAF50; /* 왼쪽 테두리 */ | |
| border-radius: 5px; | |
| text-align: left; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .generated-answer-section h2 { | |
| color: #2e7d32; /* 진한 녹색 제목 */ | |
| margin-top: 0; | |
| margin-bottom: 15px; | |
| font-size: 1.5em; | |
| } | |
| .generated-answer-section p { | |
| line-height: 1.6; | |
| color: #333; | |
| white-space: pre-wrap; /* 줄바꿈 및 공백 유지 */ | |
| } | |
| .info-message { | |
| color: #3498db; | |
| margin-top: 20px; | |
| font-style: italic; | |
| background-color: #e3f2fd; | |
| border: 1px solid #90caf9; | |
| padding: 10px; | |
| border-radius: 4px; | |
| } | |
| .timestamp-link { | |
| color: #007bff; /* 파란색 링크 스타일 */ | |
| cursor: pointer; /* 클릭 가능하다는 표시 */ | |
| text-decoration: none; /* 밑줄 */ | |
| } | |
| .timestamp-link:hover { | |
| color: #0056b3; /* 호버 시 색상 변경 */ | |
| } | |
| .results-section h2 { /* 이 스타일은 이제 .right-sidebar h2로 대체될 수 있음 */ | |
| color: #2c3e50; | |
| margin-bottom: 15px; | |
| font-size: 1.3em; | |
| } | |
| .result-item { | |
| background-color: #f9f9f9; | |
| border: 1px solid #eee; | |
| border-radius: 4px; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.05); | |
| } | |
| .result-item p { | |
| margin: 0 0 5px 0; | |
| color: #555; | |
| text-align: left; | |
| } | |
| .result-item p strong { | |
| color: #000; | |
| } | |
| .result-item a { | |
| color: #42b983; | |
| text-decoration: none; | |
| } | |
| .result-item a:hover { | |
| text-decoration: underline; | |
| } | |
| .video-embed { | |
| margin-top: 20px; | |
| margin-bottom: 30px; | |
| background-color: #000; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| position: relative; | |
| padding-bottom: 56.25%; | |
| height: 0; | |
| } | |
| .video-embed iframe { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| </style> |