Sodagraph's picture
LLM추가 및 템플릿 개선, (프록시는 미구현)
9b9d44e
raw
history blame
10.8 kB
<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>