前端代码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>情感分析系统</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> <!-- 自定义 Tailwind 配置 --> <script> tailwind.config = { theme: { extend: { colors: { primary: '#3B82F6', secondary: '#10B981', neutral: '#64748B', success: '#10B981', warning: '#F59E0B', danger: '#EF4444', emotion: { angry: '#FF4136', contempt: '#7FDBFF', disgust: '#2ECC40', fear: '#B10DC9', happy: '#FFDC00', natural: '#AAAAAA', sad: '#0074D9', sleepy: '#39CCCC', surprised: '#FF851B' } }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], }, } } } </script> <!-- 自定义工具类 --> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .card-hover { @apply transition-all duration-300 hover:shadow-lg hover:-translate-y-1; } .gradient-bg { @apply bg-gradient-to-r from-primary to-secondary; } .emotion-indicator { @apply inline-block w-3 h-3 rounded-full mr-2; } .drop-zone { @apply border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary transition-colors; } .drop-zone-video { @apply border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-secondary transition-colors; } .timeline-item { @apply bg-gray-50 p-4 rounded-lg border border-gray-200 flex items-start; } .timeline-time { @apply bg-primary text-white px-3 py-1 rounded-full text-sm font-medium mr-4; } } </style> </head> <body class="bg-gray-50 min-h-screen"> <!-- 导航栏 --> <nav class="gradient-bg text-white shadow-md fixed w-full z-10"> <div class="container mx-auto px-4 py-4 flex justify-between items-center"> <div class="flex items-center space-x-3"> <i class="fa fa-smile-o text-2xl"></i> <h1 class="text-xl md:text-2xl font-bold">情感分析系统</h1> </div> <div class="hidden md:flex items-center space-x-6"> <a href="#" class="hover:text-gray-200 transition-colors">首页</a> <a href="#" class="hover:text-gray-200 transition-colors">使用说明</a> <a href="#" class="hover:text-gray-200 transition-colors">关于我们</a> </div> <button class="md:hidden text-xl"> <i class="fa fa-bars"></i> </button> </div> </nav> <!-- 主内容区 --> <main class="container mx-auto px-4 py-8 pt-24"> <!-- 介绍卡片 --> <div class="bg-white rounded-xl shadow-md p-6 mb-8 card-hover"> <h2 class="text-2xl font-bold text-gray-800 mb-4">面部表情智能分析</h2> <p class="text-gray-600 mb-4"> 本系统基于先进的深度学习模型,能够快速准确地识别图片和视频中的面部表情。 支持分析多种常见表情:愤怒、轻蔑、厌恶、恐惧、快乐、中性、悲伤、困倦和惊讶。 </p> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6"> <div class="bg-gray-50 rounded-lg p-4 text-center"> <i class="fa fa-image text-primary text-3xl mb-2"></i> <h3 class="font-medium text-gray-800">图片分析</h3> <p class="text-sm text-gray-600">上传图片进行表情识别</p> </div> <div class="bg-gray-50 rounded-lg p-4 text-center"> <i class="fa fa-film text-secondary text-3xl mb-2"></i> <h3 class="font-medium text-gray-800">视频分析</h3> <p class="text-sm text-gray-600">分析视频中的表情变化</p> </div> <div class="bg-gray-50 rounded-lg p-4 text-center"> <i class="fa fa-bar-chart text-warning text-3xl mb-2"></i> <h3 class="font-medium text-gray-800">数据可视化</h3> <p class="text-sm text-gray-600">表情分布与趋势图表</p> </div> <div class="bg-gray-50 rounded-lg p-4 text-center"> <i class="fa fa-lightbulb-o text-success text-3xl mb-2"></i> <h3 class="font-medium text-gray-800">智能推荐</h3> <p class="text-sm text-gray-600">基于表情的内容推荐</p> </div> </div> </div> <!-- 图片上传区 --> <div class="bg-white rounded-xl shadow-md p-6 mb-8 card-hover"> <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <i class="fa fa-image text-primary mr-2"></i>图片表情分析 </h2> <div class="drop-zone" id="imageDropZone"> <form id="imageForm" class="space-y-4"> <input type="file" id="imageInput" name="image" accept="image/*" class="hidden"> <label for="imageInput" class="cursor-pointer block"> <div class="flex flex-col items-center"> <i class="fa fa-cloud-upload text-4xl text-gray-400 mb-4"></i> <p class="text-gray-600 mb-2">点击或拖拽图片到此处</p> <p class="text-sm text-gray-500">支持 JPG, PNG, GIF 格式</p> <button type="button" id="selectImageBtn" class="mt-4 bg-primary hover:bg-primary/90 text-white font-medium py-2 px-6 rounded-lg transition-all"> <i class="fa fa-upload mr-2"></i>选择图片 </button> </div> </label> <div id="imagePreview" class="hidden mt-4"> <img id="previewImg" src="" alt="预览图片" class="max-w-full h-auto rounded-lg border-2 border-gray-200"> </div> <button type="submit" id="analyzeImageBtn" class="mt-4 bg-secondary hover:bg-secondary/90 text-white font-medium py-2 px-6 rounded-lg transition-all hidden"> <i class="fa fa-eye mr-2"></i>分析表情 </button> </form> </div> </div> <!-- 图片分析结果 --> <div id="imageResultSection" class="hidden bg-white rounded-xl shadow-md p-6 mb-8 card-hover"> <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <i class="fa fa-check-circle text-success mr-2"></i>分析结果 <span id="imageAnalysisTime" class="ml-3 text-sm text-gray-500"></span> </h2> <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <!-- 原图 --> <div class="md:col-span-1"> <h3 class="text-lg font-medium text-gray-700 mb-2">原始图片</h3> <div class="bg-gray-100 rounded-lg overflow-hidden relative"> <img id="resultImage" src="" alt="分析结果图片" class="w-full h-auto"> </div> </div> <!-- 检测结果 --> <div class="md:col-span-2"> <h3 class="text-lg font-medium text-gray-700 mb-2">表情检测结果</h3> <div id="imageDetectionResults" class="space-y-4"> <!-- 结果将通过 JavaScript 动态填充 --> </div> </div> </div> </div> <!-- 视频上传区 --> <div class="bg-white rounded-xl shadow-md p-6 mb-8 card-hover"> <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <i class="fa fa-film text-secondary mr-2"></i>视频表情分析 </h2> <div class="drop-zone-video" id="videoDropZone"> <form id="videoForm" class="space-y-4"> <input type="file" id="videoInput" name="video" accept="video/*" class="hidden"> <label for="videoInput" class="cursor-pointer block"> <div class="flex flex-col items-center"> <i class="fa fa-video-camera text-4xl text-gray-400 mb-4"></i> <p class="text-gray-600 mb-2">点击或拖拽视频到此处</p> <p class="text-sm text-gray-500">支持 MP4, AVI, MOV 格式</p> <button type="button" id="selectVideoBtn" class="mt-4 bg-secondary hover:bg-secondary/90 text-white font-medium py-2 px-6 rounded-lg transition-all"> <i class="fa fa-upload mr-2"></i>选择视频 </button> </div> </label> <div id="videoPreview" class="hidden mt-4"> <video id="previewVideo" controls class="max-w-full h-auto rounded-lg border-2 border-gray-200"></video> </div> <button type="submit" id="analyzeVideoBtn" class="mt-4 bg-secondary hover:bg-secondary/90 text-white font-medium py-2 px-6 rounded-lg transition-all hidden"> <i class="fa fa-play-circle mr-2"></i>分析视频 </button> </form> </div> </div> <!-- 视频分析结果 --> <div id="videoResultSection" class="hidden bg-white rounded-xl shadow-md p-6 mb-8 card-hover"> <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <i class="fa fa-film text-secondary mr-2"></i>视频分析结果 <span id="videoAnalysisTime" class="ml-3 text-sm text-gray-500"></span> </h2> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <!-- 视频预览 --> <div class="md:col-span-1"> <h3 class="text-lg font-medium text-gray-700 mb-2">原始视频</h3> <div class="bg-gray-100 rounded-lg overflow-hidden"> <video id="resultVideo" controls class="w-full h-auto"></video> </div> </div> <!-- 统计信息 --> <div class="md:col-span-2"> <h3 class="text-lg font-medium text-gray-700 mb-2">表情统计</h3> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="bg-gray-50 rounded-lg p-4"> <h4 class="font-medium text-gray-800 mb-2">主导表情</h4> <div class="flex items-center justify-center"> <div id="dominantEmotion" class="text-3xl font-bold"></div> </div> <div class="mt-3 text-center"> <p id="videoRecommendation" class="text-gray-600"></p> </div> </div> <div class="bg-gray-50 rounded-lg p-4"> <h4 class="font-medium text-gray-800 mb-2">表情分布</h4> <div class="h-48"> <canvas id="emotionDistributionChart"></canvas> </div> </div> </div> </div> </div> <!-- 表情趋势图表 --> <div class="mb-6"> <h3 class="text-lg font-medium text-gray-700 mb-2">表情随时间变化趋势</h3> <div class="bg-gray-50 rounded-lg p-4"> <div class="h-64"> <canvas id="emotionTrendChart"></canvas> </div> </div> </div> <!-- 时间线分析 --> <div> <h3 class="text-lg font-medium text-gray-700 mb-2">关键表情时间点</h3> <div id="emotionTimeline" class="space-y-3"> <!-- 时间线将通过 JavaScript 动态填充 --> </div> </div> </div> </main> <!-- 页脚 --> <footer class="bg-gray-800 text-white py-8"> <div class="container mx-auto px-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div> <h3 class="text-xl font-bold mb-4">情感分析系统</h3> <p class="text-gray-400"> 基于先进深度学习模型的面部表情智能分析系统, 能够准确识别图片和视频中的多种表情,为您提供情感分析和内容推荐服务。 </p> </div> <div> <h3 class="text-lg font-bold mb-4">功能导航</h3> <ul class="space-y-2"> <li><a href="#" class="text-gray-400 hover:text-white transition-colors">图片分析</a></li> <li><a href="#" class="text-gray-400 hover:text-white transition-colors">视频分析</a></li> <li><a href="#" class="text-gray-400 hover:text-white transition-colors">使用说明</a></li> <li><a href="#" class="text-gray-400 hover:text-white transition-colors">关于我们</a></li> </ul> </div> <div> <h3 class="text-lg font-bold mb-4">联系我们</h3> <ul class="space-y-2"> <li class="flex items-center"> <i class="fa fa-envelope-o mr-2 text-gray-400"></i> <a href="mailto:contact@example.com" class="text-gray-400 hover:text-white transition-colors">contact@example.com</a> </li> <li class="flex items-center"> <i class="fa fa-phone mr-2 text-gray-400"></i> <span class="text-gray-400">+86 123 4567 8901</span> </li> <li class="flex items-center"> <i class="fa fa-map-marker mr-2 text-gray-400"></i> <span class="text-gray-400">北京市海淀区科技园区</span> </li> </ul> </div> </div> <div class="border-t border-gray-700 mt-8 pt-6 text-center text-gray-500"> <p>© 2023 情感分析系统. 保留所有权利.</p> </div> </div> </footer> <!-- JavaScript --> <script> document.addEventListener('DOMContentLoaded', function() { // 图片上传处理 const imageInput = document.getElementById('imageInput'); const imageForm = document.getElementById('imageForm'); const imagePreview = document.getElementById('imagePreview'); const previewImg = document.getElementById('previewImg'); const analyzeImageBtn = document.getElementById('analyzeImageBtn'); const imageResultSection = document.getElementById('imageResultSection'); const resultImage = document.getElementById('resultImage'); const imageDetectionResults = document.getElementById('imageDetectionResults'); const imageAnalysisTime = document.getElementById('imageAnalysisTime'); const selectImageBtn = document.getElementById('selectImageBtn'); const imageDropZone = document.getElementById('imageDropZone'); // 视频上传处理 const videoInput = document.getElementById('videoInput'); const videoForm = document.getElementById('videoForm'); const videoPreview = document.getElementById('videoPreview'); const previewVideo = document.getElementById('previewVideo'); const analyzeVideoBtn = document.getElementById('analyzeVideoBtn'); const videoResultSection = document.getElementById('videoResultSection'); const resultVideo = document.getElementById('resultVideo'); const videoAnalysisTime = document.getElementById('videoAnalysisTime'); const dominantEmotion = document.getElementById('dominantEmotion'); const videoRecommendation = document.getElementById('videoRecommendation'); const selectVideoBtn = document.getElementById('selectVideoBtn'); const videoDropZone = document.getElementById('videoDropZone'); // 图片上传预览 imageInput.addEventListener('change', function() { handleImageFile(this.files[0]); }); // 视频上传预览 videoInput.addEventListener('change', function() { handleVideoFile(this.files[0]); }); // 选择图片按钮 selectImageBtn.addEventListener('click', function() { imageInput.click(); }); // 选择视频按钮 selectVideoBtn.addEventListener('click', function() { videoInput.click(); }); // 拖放功能 - 图片 imageDropZone.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('border-primary', 'bg-blue-50'); }); imageDropZone.addEventListener('dragleave', function() { this.classList.remove('border-primary', 'bg-blue-50'); }); imageDropZone.addEventListener('drop', function(e) { e.preventDefault(); this.classList.remove('border-primary', 'bg-blue-50'); if (e.dataTransfer.files.length) { handleImageFile(e.dataTransfer.files[0]); } }); // 拖放功能 - 视频 videoDropZone.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('border-secondary', 'bg-green-50'); }); videoDropZone.addEventListener('dragleave', function() { this.classList.remove('border-secondary', 'bg-green-50'); }); videoDropZone.addEventListener('drop', function(e) { e.preventDefault(); this.classList.remove('border-secondary', 'bg-green-50'); if (e.dataTransfer.files.length) { handleVideoFile(e.dataTransfer.files[0]); } }); // 处理图片文件 function handleImageFile(file) { if (!file) return; const validTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!validTypes.includes(file.type)) { alert('请上传 JPG, PNG 或 GIF 格式的图片'); return; } const reader = new FileReader(); reader.onload = function(e) { previewImg.src = e.target.result; imagePreview.classList.remove('hidden'); analyzeImageBtn.classList.remove('hidden'); } reader.readAsDataURL(file); } // 处理视频文件 function handleVideoFile(file) { if (!file) return; const validTypes = ['video/mp4', 'video/avi', 'video/quicktime']; if (!validTypes.includes(file.type)) { alert('请上传 MP4, AVI 或 MOV 格式的视频'); return; } const videoUrl = URL.createObjectURL(file); previewVideo.src = videoUrl; videoPreview.classList.remove('hidden'); analyzeVideoBtn.classList.remove('hidden'); } // 图片分析 imageForm.addEventListener('submit', function(e) { e.preventDefault(); if (!imageInput.files.length && !previewImg.src) { alert('请选择一张图片'); return; } // 显示加载状态 imageResultSection.classList.remove('hidden'); imageDetectionResults.innerHTML = ` <div class="flex items-center justify-center py-12"> <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div> <span class="ml-3 text-gray-600">正在分析表情...</span> </div> `; // 创建FormData const formData = new FormData(); if (imageInput.files.length) { formData.append('image', imageInput.files[0]); } else { // 处理拖放的文件 const blob = dataURItoBlob(previewImg.src); formData.append('image', blob, 'uploaded_image.png'); } // 发送请求 fetch('/analyze-image', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error('网络响应异常'); } return response.json(); }) .then(data => { if (data.success) { // 更新结果图片 resultImage.src = data.image_url; // 更新分析时间 imageAnalysisTime.textContent = `分析耗时: ${data.inference_time.toFixed(2)}秒`; // 清空结果区域 imageDetectionResults.innerHTML = ''; // 渲染检测结果 if (data.detections.length > 0) { data.detections.forEach((detection, index) => { const confidencePercent = (detection.confidence * 100).toFixed(1); imageDetectionResults.innerHTML += ` <div class="bg-gray-50 p-4 rounded-lg border border-gray-200 card-hover"> <div class="flex justify-between items-center"> <h4 class="font-medium text-lg" style="color: ${detection.color}"> <i class="fa fa-user-circle-o mr-2"></i>人物 #${index + 1} </h4> <span class="px-3 py-1 rounded-full bg-gray-100 text-sm"> 置信度: ${confidencePercent}% </span> </div> <div class="mt-3 grid grid-cols-2 gap-2"> <div class="bg-white p-3 rounded-lg"> <p class="text-sm text-gray-500">识别表情</p> <p class="text-xl font-semibold" style="color: ${detection.color}"> <i class="fa fa-smile-o mr-2"></i>${detection.emotion} </p> </div> <div class="bg-white p-3 rounded-lg"> <p class="text-sm text-gray-500">推荐内容</p> <p class="text-gray-700">${detection.recommendation}</p> </div> </div> <div class="mt-4"> <p class="text-sm text-gray-500 mb-2">边界框坐标</p> <div class="grid grid-cols-2 gap-2 text-sm"> <div class="bg-gray-100 p-2 rounded"> <span class="font-medium">左上: </span>(${Math.round(detection.bbox[0])}, ${Math.round(detection.bbox[1])}) </div> <div class="bg-gray-100 p-2 rounded"> <span class="font-medium">右下: </span>(${Math.round(detection.bbox[2])}, ${Math.round(detection.bbox[3])}) </div> </div> </div> </div> `; }); } else { imageDetectionResults.innerHTML = ` <div class="text-center py-12 text-gray-500"> <i class="fa fa-meh-o text-3xl mb-2"></i> <p>未检测到人脸表情</p> </div> `; } } else { imageDetectionResults.innerHTML = ` <div class="text-center py-12 text-red-500"> <i class="fa fa-exclamation-triangle text-3xl mb-2"></i> <p>分析失败: ${data.error || '未知错误'}</p> </div> `; } }) .catch(error => { console.error('Error:', error); imageDetectionResults.innerHTML = ` <div class="text-center py-12 text-red-500"> <i class="fa fa-exclamation-triangle text-3xl mb-2"></i> <p>发生错误: ${error.message || '网络或服务器错误'}</p> </div> `; }); }); // 视频分析 videoForm.addEventListener('submit', function(e) { e.preventDefault(); if (!videoInput.files.length && !previewVideo.src) { alert('请选择一个视频'); return; } // 显示加载状态 videoResultSection.classList.remove('hidden'); // 清空图表容器 document.getElementById('emotionDistributionChart').remove(); document.getElementById('emotionTrendChart').remove(); document.getElementById('emotionTimeline').innerHTML = ''; // 创建新的图表容器 const distributionContainer = document.createElement('canvas'); distributionContainer.id = 'emotionDistributionChart'; document.querySelector('#emotionDistributionChart').parentNode.appendChild(distributionContainer); const trendContainer = document.createElement('canvas'); trendContainer.id = 'emotionTrendChart'; document.querySelector('#emotionTrendChart').parentNode.appendChild(trendContainer); // 显示加载状态 document.getElementById('emotionDistributionChart').parentNode.innerHTML = ` <div class="flex items-center justify-center h-full"> <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-secondary"></div> <span class="ml-3 text-gray-600">正在分析视频...</span> </div> `; document.getElementById('emotionTrendChart').parentNode.innerHTML = ` <div class="flex items-center justify-center h-full"> <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-secondary"></div> <span class="ml-3 text-gray-600">正在分析视频...</span> </div> `; document.getElementById('emotionTimeline').innerHTML = ` <div class="flex items-center justify-center py-6"> <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-secondary"></div> <span class="ml-3 text-gray-600">正在生成时间线...</span> </div> `; // 创建FormData const formData = new FormData(); if (videoInput.files.length) { formData.append('video', videoInput.files[0]); } else { // 处理拖放的文件 const blob = dataURItoBlob(previewVideo.src); formData.append('video', blob, 'uploaded_video.mp4'); } // 发送请求 fetch('/analyze-video', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error('网络响应异常'); } return response.json(); }) .then(data => { if (data.success) { // 更新结果视频 resultVideo.src = data.video_url; // 更新分析时间 videoAnalysisTime.textContent = `分析耗时: ${data.inference_time.toFixed(2)}秒`; // 更新主导表情和推荐 dominantEmotion.innerHTML = ` <span class="inline-block w-8 h-8 rounded-full mr-2" style="background-color: ${EMOTION_COLORS[data.dominant_emotion] || '#AAAAAA'}"></span> ${data.dominant_emotion} `; videoRecommendation.textContent = data.recommendation; // 渲染表情分布饼图 renderDistributionChart(data.emotion_counts); // 渲染表情趋势图 renderTrendChart(data.emotion_timeline, data.duration); // 渲染时间线 renderTimeline(data.emotion_timeline); } else { // 错误处理 document.getElementById('emotionDistributionChart').parentNode.innerHTML = ` <div class="text-center py-12 text-red-500"> <i class="fa fa-exclamation-triangle text-3xl mb-2"></i> <p>分析失败: ${data.error || '未知错误'}</p> </div> `; } }) .catch(error => { console.error('Error:', error); document.getElementById('emotionDistributionChart').parentNode.innerHTML = ` <div class="text-center py-12 text-red-500"> <i class="fa fa-exclamation-triangle text-3xl mb-2"></i> <p>发生错误: ${error.message || '网络或服务器错误'}</p> </div> `; }); }); // 渲染表情分布饼图 function renderDistributionChart(emotionCounts) { const ctx = document.getElementById('emotionDistributionChart').getContext('2d'); const labels = Object.keys(emotionCounts); const data = Object.values(emotionCounts); const backgroundColors = labels.map(emotion => EMOTION_COLORS[emotion] || '#CCCCCC'); new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: backgroundColors, borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { boxWidth: 12, padding: 15, font: { size: 12 } } } } } }); } // 渲染表情趋势图 function renderTrendChart(emotionTimeline, duration) { const ctx = document.getElementById('emotionTrendChart').getContext('2d'); // 准备数据 const emotions = Object.keys(EMOTION_CLASSES).map(key => EMOTION_CLASSES[key]); const datasets = []; emotions.forEach(emotion => { const dataPoints = emotionTimeline.map(point => { return point.emotions.includes(emotion) ? 1 : 0; }); datasets.push({ label: emotion, data: dataPoints, backgroundColor: EMOTION_COLORS[emotion] + '33', borderColor: EMOTION_COLORS[emotion], borderWidth: 2, pointRadius: 0, tension: 0.3, fill: false }); }); // 生成时间标签 const labels = emotionTimeline.map(point => point.time.toFixed(1) + 's'); new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value === 1 ? emotion : ''; } } }, x: { title: { display: true, text: '时间 (秒)' } } }, plugins: { legend: { position: 'top', }, tooltip: { callbacks: { label: function(context) { return context.dataset.label; } } } } } }); } // 渲染时间线 function renderTimeline(emotionTimeline) { const timelineContainer = document.getElementById('emotionTimeline'); timelineContainer.innerHTML = ''; emotionTimeline.forEach(point => { const emotionsHtml = point.emotions.map(emotion => { return `<span class="inline-flex items-center mr-3"> <span class="emotion-indicator" style="background-color: ${EMOTION_COLORS[emotion]}"></span> ${emotion} </span>`; }).join(''); timelineContainer.innerHTML += ` <div class="timeline-item"> <span class="timeline-time">${point.time.toFixed(1)}秒</span> <div class="flex flex-wrap"> ${emotionsHtml} </div> </div> `; }); } // 辅助函数:将DataURI转换为Blob function dataURItoBlob(dataURI) { const byteString = atob(dataURI.split(',')[1]); const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new Blob([ab], { type: mimeString }); } }); </script> </body> </html>