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> |