203 lines
11 KiB
HTML
203 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI Media Hub</title>
|
|
<!-- Tailwind CSS (CDN for minimal setup) -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
dark: '#121212',
|
|
darker: '#0a0a0a',
|
|
primary: '#ffffff',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
body { @apply bg-darker text-primary font-sans antialiased; }
|
|
.zone-card { @apply bg-dark rounded-xl border border-gray-800 p-6 shadow-2xl; }
|
|
.input-dark { @apply bg-darker border border-gray-700 rounded-lg px-4 py-3 focus:outline-none focus:border-gray-500 w-full transition-colors; }
|
|
.btn-primary { @apply bg-white text-black font-semibold rounded-lg px-6 py-3 hover:bg-gray-200 transition-colors; }
|
|
.thumbnail-card { @apply relative group rounded-lg overflow-hidden border border-gray-800 cursor-pointer; }
|
|
.thumbnail-img { @apply w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105; }
|
|
.ai-badge { @apply absolute top-2 right-2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white text-xs font-bold px-2 py-1 rounded-md shadow-lg; }
|
|
.source-badge { @apply absolute bottom-2 left-2 bg-black/70 backdrop-blur-sm text-white text-xs px-2 py-1 rounded; }
|
|
.dropzone { @apply border-2 border-dashed border-gray-700 rounded-xl p-12 text-center hover:border-gray-500 transition-colors cursor-pointer bg-dark; }
|
|
</style>
|
|
</head>
|
|
<body class="dark h-screen flex flex-col">
|
|
<!-- Header -->
|
|
<header class="border-b border-gray-800 bg-darker/80 backdrop-blur-md sticky top-0 z-50">
|
|
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
|
|
<h1 class="text-2xl font-bold tracking-tight">AI Media Hub <span class="text-sm font-normal text-gray-500 ml-2">For Video Editors</span></h1>
|
|
<div id="status-indicator" class="flex items-center space-x-2 text-sm text-gray-400">
|
|
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
|
<span>System Online</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex-1 overflow-y-auto p-6">
|
|
<div class="container mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
|
|
<!-- Zone A: AI 디스커버리 -->
|
|
<section class="lg:col-span-2 zone-card">
|
|
<h2 class="text-xl font-semibold mb-4 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
|
Zone A: AI Discovery (Gemini 2.5 Flash Vision)
|
|
</h2>
|
|
<form id="discovery-form" class="flex space-x-4 mb-6">
|
|
<input type="text" id="search-query" class="input-dark" placeholder="AI가 썸네일을 분석하여 가장 적합한 영상을 찾아줍니다... (예: 사이버펑크 스타일 도시 야경)" required>
|
|
<button type="submit" class="btn-primary whitespace-nowrap" id="search-btn">
|
|
Search
|
|
</button>
|
|
</form>
|
|
|
|
<div id="loading-spinner" class="hidden py-12 flex-col items-center justify-center text-gray-500">
|
|
<svg class="animate-spin h-8 w-8 mb-4" viewBox="0 0 24 24" fill="none">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<p>AI가 썸네일을 분석하고 있습니다...</p>
|
|
</div>
|
|
|
|
<div id="results-grid" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
<!-- AI 추천 썸네일 렌더링 영역 -->
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Zone B: 스마트 인제스트 -->
|
|
<section class="zone-card">
|
|
<h2 class="text-xl font-semibold mb-4 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
|
|
Zone B: Smart Ingest (NAS)
|
|
</h2>
|
|
<div id="dropzone" class="dropzone mb-4 flex flex-col items-center justify-center">
|
|
<svg class="w-12 h-12 text-gray-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
|
|
<p class="text-gray-400">파일을 드래그 앤 드롭 하거나 클릭하여 업로드</p>
|
|
<p class="text-gray-600 text-sm mt-2">NAS에 바로 저장됩니다.</p>
|
|
</div>
|
|
<input type="file" id="file-input" class="hidden" multiple>
|
|
</section>
|
|
|
|
<!-- Zone C: 다이렉트 다운로드 (yt-dlp) -->
|
|
<section class="zone-card">
|
|
<h2 class="text-xl font-semibold mb-4 flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
Zone C: Direct Download (yt-dlp)
|
|
</h2>
|
|
<form id="download-form" class="space-y-4">
|
|
<input type="url" id="dl-url" class="input-dark" placeholder="비디오 URL 입력 (YouTube, TikTok 등)" required>
|
|
<div class="flex space-x-4">
|
|
<input type="text" id="dl-start" class="input-dark w-1/2" placeholder="시작 구간 (옵션: 00:00:00)">
|
|
<input type="text" id="dl-end" class="input-dark w-1/2" placeholder="종료 구간 (옵션: 00:01:00)">
|
|
</div>
|
|
<button type="submit" class="btn-primary w-full">Download to NAS</button>
|
|
<!-- 진행률 표시 -->
|
|
<div id="progress-container" class="hidden mt-4">
|
|
<div class="flex justify-between text-sm text-gray-400 mb-1">
|
|
<span>Downloading...</span>
|
|
<span id="progress-text">0%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-800 rounded-full h-2">
|
|
<div id="progress-bar" class="bg-white h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// WebSocket for Progress updates
|
|
const ws = new WebSocket('ws://' + window.location.host + '/ws');
|
|
ws.onmessage = function(event) {
|
|
const data = JSON.parse(event.data);
|
|
if(data.type === 'progress') {
|
|
document.getElementById('progress-container').classList.remove('hidden');
|
|
document.getElementById('progress-bar').style.width = data.percent + '%';
|
|
document.getElementById('progress-text').innerText = data.percent + '%';
|
|
}
|
|
};
|
|
|
|
// Zone A: Search
|
|
document.getElementById('discovery-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const query = document.getElementById('search-query').value;
|
|
const btn = document.getElementById('search-btn');
|
|
const spinner = document.getElementById('loading-spinner');
|
|
const grid = document.getElementById('results-grid');
|
|
|
|
btn.disabled = true;
|
|
spinner.classList.remove('hidden');
|
|
grid.innerHTML = '';
|
|
|
|
try {
|
|
const res = await fetch('/api/search', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ query })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.results && data.results.length > 0) {
|
|
data.results.forEach(item => {
|
|
const card = document.createElement('div');
|
|
card.className = 'thumbnail-card';
|
|
card.onclick = () => window.open(item.link, '_blank');
|
|
card.innerHTML = `
|
|
<img src="${item.thumbnail}" class="thumbnail-img" alt="Thumbnail">
|
|
${item.ai_recommended ? '<div class="ai-badge">✨ AI Recommended</div>' : ''}
|
|
<div class="source-badge">${item.source}</div>
|
|
<div class="p-3 bg-dark border-t border-gray-800">
|
|
<h3 class="text-sm font-semibold truncate" title="${item.title}">${item.title}</h3>
|
|
<p class="text-xs text-gray-400 mt-1 line-clamp-2">${item.ai_reasoning || item.snippet}</p>
|
|
</div>
|
|
`;
|
|
grid.appendChild(card);
|
|
});
|
|
} else {
|
|
grid.innerHTML = '<p class="text-gray-500 col-span-full text-center">결과가 없습니다.</p>';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("검색 중 오류가 발생했습니다.");
|
|
} finally {
|
|
btn.disabled = false;
|
|
spinner.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// Zone C: Download
|
|
document.getElementById('download-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const url = document.getElementById('dl-url').value;
|
|
const start = document.getElementById('dl-start').value;
|
|
const end = document.getElementById('dl-end').value;
|
|
|
|
document.getElementById('progress-container').classList.remove('hidden');
|
|
document.getElementById('progress-bar').style.width = '0%';
|
|
|
|
try {
|
|
await fetch('/api/download', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ url, start, end })
|
|
});
|
|
// Note: progress will be updated via WebSocket
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("다운로드 요청 실패");
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|