Qwen2.5-VL与Django集成:全栈视觉分析平台
1. 为什么需要一个视觉分析平台
你有没有遇到过这样的情况:团队里有人发来一张产品截图,问"这个界面按钮布局合理吗?";或者收到几十张发票照片,需要人工核对每张的金额和税号;又或者客户上传了一段会议视频,领导让你快速总结关键决策点。这些任务看似简单,但重复做起来特别耗时,而且容易出错。
Qwen2.5-VL这类多模态模型确实很强大,能看懂图片、理解图表、分析视频,但直接调用API就像用命令行操作电脑——功能都有,就是不够方便。我们真正需要的,是一个像普通网站一样能登录、上传、查看历史记录、分享结果的完整系统。
这就是Django的价值所在。它不是要取代Qwen2.5-VL,而是给这个强大的视觉引擎装上方向盘、油门和刹车。用户不需要知道什么是API密钥、什么是base64编码,只要会用浏览器就能完成复杂的视觉分析任务。本文就带你从零开始,搭建这样一个真正可用的全栈视觉分析平台。
整个过程不需要你成为Django专家,也不要求你精通多模态模型原理。我会把每个步骤拆解得足够细,重点告诉你"为什么这么做"而不是"照着做就行"。如果你之前用过Python,哪怕只是写过简单的脚本,跟着做下来就能跑通整个系统。
2. 环境准备与项目初始化
2.1 基础环境搭建
首先确认你的开发环境已经准备好。我们需要Python 3.9或更高版本,以及pip包管理工具。在终端中运行以下命令检查:
python --version pip --version如果显示版本号,说明基础环境没问题。接下来创建一个专门的项目目录:
mkdir qwen-django-platform cd qwen-django-platform python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows虚拟环境创建完成后,安装Django和必要的依赖:
pip install django==4.2.13 requests python-decouple pillow这里选择Django 4.2.13是因为它在稳定性和新特性之间取得了很好的平衡,同时兼容性也很好。requests用于调用Qwen2.5-VL的API,python-decouple帮助我们安全地管理配置,pillow则用于处理用户上传的图片。
2.2 创建Django项目与应用
现在开始创建Django项目:
django-admin startproject vision_platform . python manage.py startapp core python manage.py startapp analysis项目结构会变成这样:
qwen-django-platform/ ├── manage.py ├── vision_platform/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── asgi.py ├── core/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── views.py │ └── ... ├── analysis/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── views.py │ └── ...打开vision_platform/settings.py,找到INSTALLED_APPS,添加我们刚创建的应用:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'core', 'analysis', ]同时在settings.py底部添加媒体文件配置,因为我们要处理用户上传的图片和视频:
# 在settings.py文件末尾添加 import os from pathlib import Path # 媒体文件配置 MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 静态文件配置(如果还没有的话) STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')2.3 数据库迁移与管理员账户
Django默认使用SQLite数据库,对于我们的视觉分析平台初期完全够用。运行迁移命令创建数据库表:
python manage.py makemigrations python manage.py migrate然后创建一个管理员账户,方便后续登录后台管理:
python manage.py createsuperuser按照提示输入用户名、邮箱和密码。记住这个账户信息,稍后我们会用它登录Django管理后台。
3. 用户管理与权限系统
3.1 扩展用户模型
Django自带的用户系统很强大,但我们需要为视觉分析平台添加一些特定字段,比如用户是否开通了高级分析功能、剩余分析次数等。在core/models.py中添加自定义用户扩展:
# core/models.py from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') bio = models.TextField(max_length=500, blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) analysis_quota = models.IntegerField(default=100) # 每月分析配额 used_quota = models.IntegerField(default=0) # 已使用配额 is_premium = models.BooleanField(default=False) # 是否高级用户 def __str__(self): return f"{self.user.username}'s profile" @property def remaining_quota(self): return self.analysis_quota - self.used_quota @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): instance.profile.save()这个扩展模型为每个用户添加了头像、个人简介、分析配额等字段。remaining_quota属性让我们可以轻松计算用户还剩多少次分析机会。
3.2 用户注册与登录视图
在core/views.py中创建用户注册和登录的视图:
# core/views.py from django.shortcuts import render, redirect from django.contrib.auth import login, authenticate, logout from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.decorators import login_required from django.contrib import messages from django.urls import reverse def register_view(request): if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() username = form.cleaned_data.get('username') messages.success(request, f'欢迎 {username}!您的账户已创建成功。') login(request, user) return redirect('core:dashboard') else: form = UserCreationForm() return render(request, 'core/register.html', {'form': form}) def login_view(request): if request.method == 'POST': form = AuthenticationForm(request, data=request.POST) if form.is_valid(): username = form.cleaned_data.get('username') password = form.cleaned_data.get('password') user = authenticate(username=username, password=password) if user is not None: login(request, user) messages.info(request, f'欢迎回来,{username}!') return redirect('core:dashboard') else: form = AuthenticationForm() return render(request, 'core/login.html', {'form': form}) def logout_view(request): logout(request) messages.info(request, '您已成功退出登录。') return redirect('core:login') @login_required def dashboard_view(request): return render(request, 'core/dashboard.html')3.3 URL路由配置
在core/urls.py中配置用户相关的URL:
# core/urls.py from django.urls import path from . import views app_name = 'core' urlpatterns = [ path('register/', views.register_view, name='register'), path('login/', views.login_view, name='login'), path('logout/', views.logout_view, name='logout'), path('dashboard/', views.dashboard_view, name='dashboard'), ]同时在主项目的vision_platform/urls.py中包含这个应用的URL:
# vision_platform/urls.py from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('core/', include('core.urls')), path('analysis/', include('analysis.urls')), path('', include('core.urls')), # 根路径指向核心应用 ] # 添加媒体文件服务(仅用于开发环境) if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)3.4 创建模板文件
在项目根目录下创建模板文件夹结构:
mkdir -p templates/core创建templates/core/base.html作为基础模板:
<!-- templates/core/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}视觉分析平台{% endblock %}</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="{% url 'core:dashboard' %}">视觉分析平台</a> <div class="navbar-nav ms-auto"> {% if user.is_authenticated %} <span class="navbar-text me-3">欢迎,{{ user.username }}!</span> <a class="nav-link" href="{% url 'core:dashboard' %}">仪表板</a> <a class="nav-link" href="{% url 'analysis:upload' %}">开始分析</a> <a class="nav-link" href="{% url 'core:logout' %}">退出</a> {% else %} <a class="nav-link" href="{% url 'core:login' %}">登录</a> <a class="nav-link" href="{% url 'core:register' %}">注册</a> {% endif %} </div> </div> </nav> <div class="container mt-4"> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close"><!-- templates/core/login.html --> {% extends 'core/base.html' %} {% block title %}用户登录 - 视觉分析平台{% endblock %} {% block content %} <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header"> <h4>用户登录</h4> </div> <div class="card-body"> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-primary w-100">登录</button> </form> <div class="mt-3 text-center"> <p>还没有账户?<a href="{% url 'core:register' %}">立即注册</a></p> </div> </div> </div> </div> </div> {% endblock %}创建templates/core/register.html:
<!-- templates/core/register.html --> {% extends 'core/base.html' %} {% block title %}用户注册 - 视觉分析平台{% endblock %} {% block content %} <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header"> <h4>用户注册</h4> </div> <div class="card-body"> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-success w-100">注册账户</button> </form> <div class="mt-3 text-center"> <p>已有账户?<a href="{% url 'core:login' %}">立即登录</a></p> </div> </div> </div> </div> </div> {% endblock %}创建templates/core/dashboard.html:
<!-- templates/core/dashboard.html --> {% extends 'core/base.html' %} {% block title %}仪表板 - 视觉分析平台{% endblock %} {% block content %} <div class="row"> <div class="col-md-8"> <div class="card"> <div class="card-header"> <h4>欢迎回来,{{ user.username }}!</h4> </div> <div class="card-body"> <p>这里是您的个人仪表板。您可以:</p> <ul> <li>点击导航栏的"开始分析"上传图片或视频进行视觉分析</li> <li>查看和管理您的分析历史记录</li> <li>更新个人资料和头像</li> <li>查看剩余分析配额</li> </ul> <div class="mt-3"> <a href="{% url 'analysis:upload' %}" class="btn btn-primary">立即开始分析</a> </div> </div> </div> </div> <div class="col-md-4"> <div class="card"> <div class="card-header"> <h5>账户信息</h5> </div> <div class="card-body"> <p><strong>用户名:</strong>{{ user.username }}</p> <p><strong>邮箱:</strong>{{ user.email }}</p> <p><strong>剩余配额:</strong>{{ user.profile.remaining_quota }} 次</p> <p><strong>账户类型:</strong>{% if user.profile.is_premium %}高级用户{% else %}普通用户{% endif %}</p> </div> </div> </div> </div> {% endblock %}4. 视觉分析核心功能实现
4.1 分析任务模型设计
在analysis/models.py中创建分析任务模型:
# analysis/models.py from django.db import models from django.contrib.auth.models import User from django.utils import timezone class AnalysisTask(models.Model): STATUS_CHOICES = [ ('pending', '等待处理'), ('processing', '处理中'), ('completed', '已完成'), ('failed', '失败'), ] user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks') task_type = models.CharField(max_length=50, choices=[ ('image_description', '图片描述'), ('visual_qa', '视觉问答'), ('object_detection', '物体检测'), ('document_analysis', '文档分析'), ('video_summary', '视频摘要'), ]) image = models.ImageField(upload_to='uploads/images/', blank=True, null=True) video = models.FileField(upload_to='uploads/videos/', blank=True, null=True) question = models.TextField(blank=True, help_text="针对图片或视频的问题") result = models.JSONField(blank=True, null=True, help_text="分析结果JSON格式") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) processing_time = models.FloatField(default=0.0, help_text="处理耗时(秒)") def __str__(self): return f"{self.user.username} - {self.get_task_type_display()} - {self.created_at.strftime('%Y-%m-%d')}" class Meta: ordering = ['-created_at']这个模型涵盖了视觉分析平台的核心数据结构:用户关联、任务类型、上传的媒体文件、用户提出的问题、分析结果、状态跟踪等。
4.2 Qwen2.5-VL API封装
在analysis/utils.py中创建Qwen2.5-VL API的封装类:
# analysis/utils.py import requests import json import base64 import time from io import BytesIO from PIL import Image from django.conf import settings from django.core.files.base import ContentFile class Qwen25VLClient: def __init__(self, api_key=None, base_url=None): self.api_key = api_key or getattr(settings, 'DASHSCOPE_API_KEY', '') self.base_url = base_url or getattr(settings, 'DASHSCOPE_BASE_URL', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation') if not self.api_key: raise ValueError("DASHSCOPE_API_KEY must be set in Django settings") def _encode_image(self, image_file): """将图片文件转换为base64编码""" try: # 如果是Django的InMemoryUploadedFile,先读取内容 if hasattr(image_file, 'read'): image_file.seek(0) image_data = image_file.read() else: with open(image_file, "rb") as f: image_data = f.read() return base64.b64encode(image_data).decode("utf-8") except Exception as e: raise ValueError(f"图片编码失败: {str(e)}") def _get_image_mime_type(self, image_file): """获取图片MIME类型""" if hasattr(image_file, 'content_type'): return image_file.content_type elif hasattr(image_file, 'name'): if image_file.name.lower().endswith('.png'): return 'image/png' elif image_file.name.lower().endswith('.jpg') or image_file.name.lower().endswith('.jpeg'): return 'image/jpeg' elif image_file.name.lower().endswith('.webp'): return 'image/webp' return 'image/jpeg' def analyze_image(self, image_file, task_type='image_description', question=''): """分析单张图片""" try: # 编码图片 base64_image = self._encode_image(image_file) mime_type = self._get_image_mime_type(image_file) # 构建请求数据 if task_type == 'image_description': prompt = "请详细描述这张图片的内容,包括主要物体、场景、颜色、布局等细节。" elif task_type == 'visual_qa': prompt = question or "请回答关于这张图片的问题。" elif task_type == 'object_detection': prompt = "请定位图片中的所有物体,并以JSON格式输出每个物体的边界框坐标和标签。" elif task_type == 'document_analysis': prompt = "请分析这张文档图片,提取其中的关键信息并保持原始格式。" else: prompt = "请分析这张图片。" payload = { "model": "qwen2.5-vl-plus", # 使用Qwen2.5-VL系列模型 "input": { "messages": [ { "role": "user", "content": [ { "image": f"data:{mime_type};base64,{base64_image}" }, { "text": prompt } ] } ] } } headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" } # 发送请求 start_time = time.time() response = requests.post( self.base_url, headers=headers, json=payload, timeout=120 ) end_time = time.time() if response.status_code == 200: result = response.json() processing_time = end_time - start_time # 提取结果 try: content = result['output']['choices'][0]['message']['content'][0]['text'] return { 'success': True, 'result': content, 'processing_time': processing_time, 'raw_response': result } except (KeyError, IndexError) as e: return { 'success': False, 'error': f"解析响应失败: {str(e)}", 'raw_response': result } else: return { 'success': False, 'error': f"API请求失败: {response.status_code} - {response.text}", 'raw_response': response.json() if response.content else {} } except requests.exceptions.Timeout: return {'success': False, 'error': '请求超时,请重试'} except requests.exceptions.ConnectionError: return {'success': False, 'error': '网络连接错误,请检查网络设置'} except Exception as e: return {'success': False, 'error': f'处理过程中发生错误: {str(e)}'} def analyze_video(self, video_file, task_type='video_summary', question=''): """分析视频(简化版,实际中可能需要先抽帧)""" # 实际生产环境中,视频分析需要更复杂的处理逻辑 # 这里提供一个简化的占位实现 return { 'success': False, 'error': '视频分析功能需要额外的预处理步骤,当前版本暂不支持直接上传视频文件。建议先将视频转换为图片序列。' }4.3 任务处理视图
在analysis/views.py中创建分析任务的视图:
# analysis/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.core.files.storage import default_storage from django.core.files.base import ContentFile from django.conf import settings import json import os from .models import AnalysisTask from .utils import Qwen25VLClient from .forms import AnalysisTaskForm def upload_view(request): if request.method == 'POST': form = AnalysisTaskForm(request.POST, request.FILES) if form.is_valid(): task = form.save(commit=False) task.user = request.user # 检查配额 if request.user.profile.remaining_quota <= 0: messages.error(request, '您的分析配额已用完,请联系管理员或升级账户。') return redirect('analysis:upload') # 保存任务到数据库 task.status = 'pending' task.save() # 更新用户配额 request.user.profile.used_quota += 1 request.user.profile.save() # 异步处理任务(这里简化为同步处理,实际应使用Celery等异步任务队列) try: client = Qwen25VLClient() if task.image: result = client.analyze_image( task.image, task_type=task.task_type, question=task.question ) if result['success']: task.result = { 'text': result['result'], 'processing_time': result['processing_time'], 'status': 'completed' } task.status = 'completed' task.processing_time = result['processing_time'] else: task.result = { 'error': result['error'], 'status': 'failed' } task.status = 'failed' elif task.video: result = client.analyze_video( task.video, task_type=task.task_type, question=task.question ) if result['success']: task.result = { 'text': result['result'], 'processing_time': result['processing_time'], 'status': 'completed' } task.status = 'completed' task.processing_time = result['processing_time'] else: task.result = { 'error': result['error'], 'status': 'failed' } task.status = 'failed' else: task.result = {'error': '未提供有效的媒体文件', 'status': 'failed'} task.status = 'failed' except Exception as e: task.result = {'error': f'处理异常: {str(e)}', 'status': 'failed'} task.status = 'failed' task.save() messages.success(request, '分析任务已提交,结果将在处理完成后显示。') return redirect('analysis:task_detail', task_id=task.id) else: form = AnalysisTaskForm() return render(request, 'analysis/upload.html', {'form': form}) def task_detail_view(request, task_id): task = get_object_or_404(AnalysisTask, id=task_id, user=request.user) return render(request, 'analysis/task_detail.html', {'task': task}) def task_list_view(request): tasks = AnalysisTask.objects.filter(user=request.user).order_by('-created_at') return render(request, 'analysis/task_list.html', {'tasks': tasks})4.4 表单与模板
在analysis/forms.py中创建分析任务表单:
# analysis/forms.py from django import forms from .models import AnalysisTask class AnalysisTaskForm(forms.ModelForm): class Meta: model = AnalysisTask fields = ['task_type', 'image', 'video', 'question'] widgets = { 'task_type': forms.Select(attrs={'class': 'form-select'}), 'question': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), } def clean(self): cleaned_data = super().clean() image = cleaned_data.get('image') video = cleaned_data.get('video') if not image and not video: raise forms.ValidationError('请至少上传一张图片或一个视频文件。') if image and video: raise forms.ValidationError('请只上传图片或视频中的一种,不要同时上传。') return cleaned_data创建模板文件:
mkdir -p templates/analysis创建templates/analysis/upload.html:
<!-- templates/analysis/upload.html --> {% extends 'core/base.html' %} {% block title %}上传分析 - 视觉分析平台{% endblock %} {% block content %} <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header"> <h4>上传媒体文件进行视觉分析</h4> </div> <div class="card-body"> <form method="post" enctype="multipart/form-data"> {% csrf_token %} <div class="mb-3"> <label for="{{ form.task_type.id_for_label }}" class="form-label">分析类型</label> {{ form.task_type }} </div> <div class="mb-3"> <label for="{{ form.image.id_for_label }}" class="form-label">上传图片</label> <div class="input-group"> <input type="file" class="form-control" id="{{ form.image.id_for_label }}" name="{{ form.image.name }}"> <label class="input-group-text" for="{{ form.image.id_for_label }}">浏览</label> </div> <div class="form-text">支持JPG、PNG、WEBP格式,最大5MB</div> </div> <div class="mb-3"> <label for="{{ form.video.id_for_label }}" class="form-label">上传视频(可选)</label> <div class="input-group"> <input type="file" class="form-control" id="{{ form.video.id_for_label }}" name="{{ form.video.name }}"> <label class="input-group-text" for="{{ form.video.id_for_label }}">浏览</label> </div> <div class="form-text">支持MP4格式,最大50MB(视频分析需额外处理)</div> </div> <div class="mb-3"> <label for="{{ form.question.id_for_label }}" class="form-label">问题(可选)</label> {{ form.question }} <div class="form-text">例如:"这张图片中有哪些人物?他们在做什么?"</div> </div> <button type="submit" class="btn btn-primary">提交分析任务</button> <a href="{% url 'core:dashboard' %}" class="btn btn-secondary ms-2">返回仪表板</a> </form> </div> </div> </div> </div> {% endblock %}创建templates/analysis/task_detail.html:
<!-- templates/analysis/task_detail.html --> {% extends 'core/base.html' %} {% block title %}分析结果 - 视觉分析平台{% endblock %} {% block content %} <div class="row"> <div class="col-md-8"> <div class="card"> <div class="card-header"> <h4>分析任务详情</h4> </div> <div class="card-body"> <div class="mb-3"> <h5>任务信息</h5> <p><strong>任务类型:</strong>{{ task.get_task_type_display }}</p> <p><strong>创建时间:</strong>{{ task.created_at|date:"Y-m-d H:i:s" }}</p> <p><strong>状态:</strong> {% if task.status == 'completed' %} <span class="badge bg-success">{{ task.get_status_display }}</span> {% elif task.status == 'failed' %} <span class="badge bg-danger">{{ task.get_status_display }}</span> {% elif task.status == 'processing' %} <span class="badge bg-warning">{{ task.get_status_display }}</span> {% else %} <span class="badge bg-info">{{ task.get_status_display }}</span> {% endif %} </p> {% if task.processing_time > 0 %} <p><strong>处理耗时:</strong>{{ task.processing_time|floatformat:2 }} 秒</p> {% endif %} </div> <div class="mb-3"> <h5>上传的文件</h5> {% if task.image %} <div class="mb-2"> <img src="{{ task.image.url }}" alt="上传的图片" class="img-fluid rounded" style="max-height: 300px;"> </div> <p><strong>图片文件:</strong>{{ task.image.name }}</p> {% elif task.video %} <p><strong>视频文件:</strong>{{ task.video.name }}</p> <p class="text-muted">视频分析需要额外的预处理步骤,当前显示的是处理状态。</p> {% endif %} </div> {% if task.question %} <div class="mb-3"> <h5>您的问题</h5> <p class="bg-light p-3 rounded">{{ task.question }}</p> </div> {% endif %} <div class="mb-3"> <h5>分析结果</h5> {% if task.status == 'completed' and task.result %} <div class="bg-light p-3 rounded"> <p>{{ task.result.text|linebreaks }}</p> {% if task.result.processing_time %} <small class="text-muted">处理耗时:{{ task.result.processing_time|floatformat:2 }} 秒</small> {% endif %} </div> {% elif task.status == 'failed' and task.result %} <div class="alert alert-danger"> <h6>分析失败</h6> <p>{{ task.result.error }}</p> </div> {% else %} <div class="alert alert-info"> <h6>任务正在处理中</h6> <p>请稍等片刻,结果生成后会自动显示在这里。</p> </div> {% endif %} </div> <div class="d-flex gap-2"> <a href="{% url 'analysis:upload' %}" class="btn btn-primary">新建分析任务</a> <a href="{% url 'analysis:task_list' %}" class="btn btn-outline-secondary">查看所有任务</a> </div> </div> </div> </div> <div class="col-md-4"> <div class="card"> <div class="card-header"> <h5>分析类型说明</h5> </div> <div class="card-body"> <ul class="list-unstyled"> <li class="mb-2"><strong>图片描述:</strong>让AI详细描述图片内容</li> <li class="mb-2"><strong>视觉问答:</strong>针对图片提出具体问题</li> <li class="mb-2"><strong>物体检测:</strong>识别并定位图片中的物体</li> <li class="mb-2"><strong>文档分析:</strong>提取发票、表格等文档信息</li> <li class="mb-2"><strong>视频摘要:</strong>生成视频内容摘要(需额外处理)</li> </ul> </div> </div> </div> </div> {% endblock %}创建templates/analysis/task_list.html:
<!-- templates/analysis/task_list.html --> {% extends 'core/base.html' %} {% block title %}分析历史 - 视觉分析平台{% endblock %} {% block content %} <div class="row"> <div class="col-md-12"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h4>分析历史记录</h4> <a href="{% url 'analysis:upload' %}" class="btn btn-primary">新建分析任务</a> </div> <div class="card-body"> {% if tasks %} <div class="table-responsive"> <table class="table table-hover"> <thead> <tr> <th>任务ID</th> <th>类型</th> <th>文件</th> <th>状态</th> <th>时间</th> <th>操作</th> </tr> </thead> <tbody> {% for task in tasks %} <tr> <td>{{ task.id }}</td> <td>{{ task.get_task_type_display }}</td> <td> {% if task.image %} <span class="badge bg-primary">图片</span> {% elif task.video %} <span class="badge bg-info">视频</span> {% endif %} </td> <td> {% if task.status == 'completed' %} <span class="badge bg-success">{{ task.get_status_display }}</span> {% elif task.status == 'failed' %} <span class="badge bg-danger">{{ task.get_status_display }}</span> {% elif task.status == 'processing' %} <span class="badge bg-warning">{{ task.get_status_display }}</span> {% else %} <span class="badge bg-info">{{ task.get_status_display }}</span> {% endif %} </td> <td>{{ task.created_at|date:"Y-m-d H:i" }}</td> <td> <a href="{% url 'analysis:task_detail' task.id %}" class="btn btn-sm btn-outline-primary">查看</a> </td> </tr> {%