|
@@ -0,0 +1,265 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>视频对象检测</title>
|
|
|
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
|
|
+ <!-- 使用更可靠的 Vue 2 CDN -->
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
|
|
|
+ <style>
|
|
|
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: 'Inter', sans-serif;
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-area {
|
|
|
+ border: 2px dashed #e5e7eb;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-area:hover {
|
|
|
+ border-color: #6366f1;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ transform: translateY(-2px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .analyzing {
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes pulse {
|
|
|
+ 0% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+ 100% { opacity: 1; }
|
|
|
+ }
|
|
|
+
|
|
|
+ .gradient-bg {
|
|
|
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .glass-card {
|
|
|
+ background: rgba(255, 255, 255, 0.8);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .results-container::-webkit-scrollbar {
|
|
|
+ width: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .results-container::-webkit-scrollbar-track {
|
|
|
+ background: #f1f1f1;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .results-container::-webkit-scrollbar-thumb {
|
|
|
+ background: #c7d2fe;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .results-container::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #818cf8;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body class="min-h-screen py-8">
|
|
|
+ <div id="app" class="max-w-4xl mx-auto px-4">
|
|
|
+ <div class="glass-card rounded-2xl shadow-xl overflow-hidden">
|
|
|
+ <div class="gradient-bg px-6 py-4">
|
|
|
+ <h1 class="text-2xl font-semibold text-white">视频对象检测</h1>
|
|
|
+ <p class="text-indigo-100 text-sm mt-1">上传视频并检测其中的特定对象</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="p-6">
|
|
|
+ <form @submit.prevent="analyzeVideo" class="space-y-6">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 mb-2">上传视频</label>
|
|
|
+ <div class="upload-area rounded-xl p-8 cursor-pointer"
|
|
|
+ @click="triggerFileInput"
|
|
|
+ @dragover.prevent="handleDragOver"
|
|
|
+ @dragleave.prevent="handleDragLeave"
|
|
|
+ @drop.prevent="handleDrop"
|
|
|
+ :class="{'border-indigo-500 bg-indigo-50': isDragging}">
|
|
|
+ <div v-if="!selectedFile" class="text-center">
|
|
|
+ <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
|
|
+ <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4-4m4-4h.01" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
+ </svg>
|
|
|
+ <p class="mt-4 text-sm text-gray-600">点击或拖拽视频文件到此处上传</p>
|
|
|
+ </div>
|
|
|
+ <div v-else class="text-center">
|
|
|
+ <svg class="mx-auto h-12 w-12 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"></path>
|
|
|
+ </svg>
|
|
|
+ <p class="mt-4 text-sm text-indigo-600">{{ selectedFile.name }}</p>
|
|
|
+ <p class="text-xs text-gray-500 mt-1">{{ formatFileSize(selectedFile.size) }}</p>
|
|
|
+ </div>
|
|
|
+ <input type="file" ref="fileInput" accept="video/*" class="hidden" @change="handleFileChange">
|
|
|
+ </div>
|
|
|
+ <p v-if="selectedFile" class="mt-2 text-sm text-indigo-600">已选择文件: {{ selectedFile.name }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <label for="objectInput" class="block text-sm font-medium text-gray-700 mb-2">要查找的对象</label>
|
|
|
+ <input type="text"
|
|
|
+ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all"
|
|
|
+ id="objectInput"
|
|
|
+ v-model="objectToFind"
|
|
|
+ placeholder="例如: '穿红色衣服的人'">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="errorMessage" class="rounded-lg bg-red-50 p-4 text-red-700">
|
|
|
+ {{ errorMessage }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button type="submit"
|
|
|
+ class="w-full bg-indigo-600 text-white py-2 px-4 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all"
|
|
|
+ :disabled="isAnalyzing"
|
|
|
+ :class="{'opacity-50 cursor-not-allowed': isAnalyzing}">
|
|
|
+ {{ isAnalyzing ? '分析中...' : '开始分析' }}
|
|
|
+ </button>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div v-if="isAnalyzing" class="text-center mt-6">
|
|
|
+ <div class="inline-flex items-center px-4 py-2 bg-indigo-100 text-indigo-700 rounded-full analyzing">
|
|
|
+ <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-indigo-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
+ <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>
|
|
|
+ 正在分析视频...
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="results-container mt-6 space-y-4 max-h-[600px] overflow-y-auto pr-2">
|
|
|
+ <div v-for="(frame, index) in results" :key="index" class="glass-card rounded-xl overflow-hidden">
|
|
|
+ <div class="p-4 border-b border-gray-100">
|
|
|
+ <h3 class="font-medium text-gray-900">第 {{ frame.second }} 秒的帧</h3>
|
|
|
+ </div>
|
|
|
+ <div class="p-4">
|
|
|
+ <img :src="frame.frame_path" :alt="'Frame ' + frame.second" class="w-full rounded-lg">
|
|
|
+ <p class="mt-4 text-gray-700">{{ frame.description || '暂无描述' }}</p>
|
|
|
+ <div class="mt-4 flex items-center justify-between text-sm text-gray-500">
|
|
|
+ <span>置信度: {{ frame.confidence }}/10</span>
|
|
|
+ <span class="px-2 py-1 rounded-full" :class="frame.is_match ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'">
|
|
|
+ {{ frame.is_match ? '匹配' : '不匹配' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
+ new Vue({
|
|
|
+ el: '#app',
|
|
|
+ data: {
|
|
|
+ selectedFile: null,
|
|
|
+ objectToFind: '',
|
|
|
+ isAnalyzing: false,
|
|
|
+ errorMessage: '',
|
|
|
+ isDragging: false,
|
|
|
+ results: []
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ triggerFileInput() {
|
|
|
+ this.$refs.fileInput.click();
|
|
|
+ },
|
|
|
+ handleFileChange(e) {
|
|
|
+ if (e.target.files.length) {
|
|
|
+ const file = e.target.files[0];
|
|
|
+ if (!file.type.startsWith('video/')) {
|
|
|
+ this.errorMessage = '请上传有效的视频文件';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.selectedFile = file;
|
|
|
+ this.errorMessage = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleDragOver() {
|
|
|
+ this.isDragging = true;
|
|
|
+ },
|
|
|
+ handleDragLeave() {
|
|
|
+ this.isDragging = false;
|
|
|
+ },
|
|
|
+ handleDrop(e) {
|
|
|
+ this.isDragging = false;
|
|
|
+ const files = e.dataTransfer.files;
|
|
|
+ if (files.length) {
|
|
|
+ const file = files[0];
|
|
|
+ if (!file.type.startsWith('video/')) {
|
|
|
+ this.errorMessage = '请上传有效的视频文件';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.selectedFile = file;
|
|
|
+ this.errorMessage = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatFileSize(bytes) {
|
|
|
+ if (bytes === 0) return '0 Bytes';
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
+ },
|
|
|
+ async analyzeVideo() {
|
|
|
+ if (!this.selectedFile) {
|
|
|
+ this.errorMessage = '请选择视频文件';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.objectToFind.trim()) {
|
|
|
+ this.errorMessage = '请输入要查找的对象描述';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isAnalyzing = true;
|
|
|
+ this.errorMessage = '';
|
|
|
+ this.results = [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 这里是模拟API请求,实际使用时替换为真实API调用
|
|
|
+ // 模拟延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
+
|
|
|
+ // 模拟返回结果
|
|
|
+ const mockResults = [
|
|
|
+ {
|
|
|
+ second: 5,
|
|
|
+ frame_path: 'https://via.placeholder.com/640x360?text=视频帧+5秒',
|
|
|
+ description: '检测到穿红色衣服的人',
|
|
|
+ confidence: 8,
|
|
|
+ is_match: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ second: 12,
|
|
|
+ frame_path: 'https://via.placeholder.com/640x360?text=视频帧+12秒',
|
|
|
+ description: '检测到多个对象',
|
|
|
+ confidence: 5,
|
|
|
+ is_match: false
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 模拟流式响应
|
|
|
+ for (const result of mockResults) {
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
+ this.results.unshift(result);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('分析错误:', error);
|
|
|
+ this.errorMessage = '分析过程中发生错误: ' + error.message;
|
|
|
+ } finally {
|
|
|
+ this.isAnalyzing = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|