diff --git a/backend/ai_services/admin.py b/backend/ai_services/admin.py index 9ce59c8..bcc27ff 100644 --- a/backend/ai_services/admin.py +++ b/backend/ai_services/admin.py @@ -1,7 +1,16 @@ from django.contrib import admin -from django.contrib.admin import ModelAdmin from unfold.admin import ModelAdmin as UnfoldModelAdmin -from .models import TranscriptionTask +from unfold.admin import StackedInline as UnfoldStackedInline +from .models import TranscriptionTask, AIEvaluation + +class AIEvaluationInline(UnfoldStackedInline): + model = AIEvaluation + extra = 0 + can_delete = False + verbose_name = "AI评估" + verbose_name_plural = "AI评估" + readonly_fields = ['created_at', 'updated_at', 'raw_response', 'reasoning'] + fields = ('score', 'evaluation', 'model_selection', 'prompt', 'reasoning', 'status', 'error_message') @admin.register(TranscriptionTask) class TranscriptionTaskAdmin(UnfoldModelAdmin): @@ -9,3 +18,24 @@ class TranscriptionTaskAdmin(UnfoldModelAdmin): list_filter = ['status', 'created_at'] search_fields = ['id', 'task_id', 'transcription', 'summary'] readonly_fields = ['id', 'created_at', 'updated_at', 'task_id'] + inlines = [AIEvaluationInline] + +@admin.register(AIEvaluation) +class AIEvaluationAdmin(UnfoldModelAdmin): + list_display = ['id', 'task', 'score', 'status', 'model_selection', 'created_at'] + list_filter = ['status', 'model_selection', 'created_at'] + search_fields = ['task__id', 'evaluation', 'reasoning'] + readonly_fields = ['id', 'created_at', 'updated_at', 'raw_response'] + fieldsets = ( + (None, { + 'fields': ('task', 'status', 'score', 'evaluation') + }), + ('配置', { + 'fields': ('model_selection', 'prompt'), + 'classes': ('collapse',), + }), + ('调试信息', { + 'fields': ('raw_response', 'reasoning', 'error_message'), + 'classes': ('collapse',), + }), + ) diff --git a/backend/ai_services/bailian_service.py b/backend/ai_services/bailian_service.py new file mode 100644 index 0000000..b9ecef5 --- /dev/null +++ b/backend/ai_services/bailian_service.py @@ -0,0 +1,98 @@ +import logging +import json +import os +from django.conf import settings +from openai import OpenAI +from .models import AIEvaluation + +logger = logging.getLogger(__name__) + +class BailianService: + def __init__(self): + self.api_key = getattr(settings, 'DASHSCOPE_API_KEY', None) + if not self.api_key: + self.api_key = os.environ.get("DASHSCOPE_API_KEY") + + if self.api_key: + self.client = OpenAI( + api_key=self.api_key, + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" + ) + else: + self.client = None + logger.warning("DASHSCOPE_API_KEY not configured.") + + def evaluate_task(self, evaluation: AIEvaluation): + """ + 执行AI评估 + """ + if not self.client: + evaluation.status = AIEvaluation.Status.FAILED + evaluation.error_message = "服务未配置 (DASHSCOPE_API_KEY missing)" + evaluation.save() + return + + task = evaluation.task + if not task.transcription: + evaluation.status = AIEvaluation.Status.FAILED + evaluation.error_message = "关联任务无逐字稿内容" + evaluation.save() + return + + evaluation.status = AIEvaluation.Status.PROCESSING + evaluation.save() + + try: + prompt = evaluation.prompt + content = task.transcription + + # 截断过长的内容以防止超出Token限制 (简单处理,取前10000字) + if len(content) > 10000: + content = content[:10000] + "...(内容过长已截断)" + + # Construct messages + messages = [ + {'role': 'system', 'content': 'You are a helpful assistant designed to output JSON.'}, + {'role': 'user', 'content': f"{prompt}\n\n以下是需要评估的内容:\n{content}"} + ] + + completion = self.client.chat.completions.create( + model=evaluation.model_selection, + messages=messages, + response_format={"type": "json_object"} + ) + + response_content = completion.choices[0].message.content + # Convert to dict for storage + raw_response = completion.model_dump() + + evaluation.raw_response = raw_response + + # Parse JSON + try: + result = json.loads(response_content) + evaluation.score = result.get('score') + evaluation.evaluation = result.get('evaluation') or result.get('comment') + + # 尝试获取推理过程(如果模型返回了) + evaluation.reasoning = result.get('reasoning') or result.get('analysis') + + if not evaluation.reasoning: + # 如果JSON里没有,把整个JSON作为推理参考 + evaluation.reasoning = json.dumps(result, ensure_ascii=False, indent=2) + + evaluation.status = AIEvaluation.Status.COMPLETED + except json.JSONDecodeError: + evaluation.status = AIEvaluation.Status.FAILED + evaluation.error_message = f"无法解析JSON响应: {response_content}" + evaluation.reasoning = response_content + + evaluation.save() + return evaluation + + except Exception as e: + logger.error(f"AI Evaluation failed: {e}") + evaluation.status = AIEvaluation.Status.FAILED + evaluation.error_message = str(e) + evaluation.save() + return evaluation diff --git a/backend/ai_services/migrations/0004_remove_transcriptiontask_evaluation_and_more.py b/backend/ai_services/migrations/0004_remove_transcriptiontask_evaluation_and_more.py new file mode 100644 index 0000000..9fd26b7 --- /dev/null +++ b/backend/ai_services/migrations/0004_remove_transcriptiontask_evaluation_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 6.0.1 on 2026-03-11 12:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ai_services', '0003_transcriptiontask_auto_chapters_data_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='transcriptiontask', + name='evaluation', + ), + migrations.RemoveField( + model_name='transcriptiontask', + name='score', + ), + migrations.CreateModel( + name='AIEvaluation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('score', models.IntegerField(blank=True, help_text='0-100分', null=True, verbose_name='AI评分')), + ('evaluation', models.TextField(blank=True, null=True, verbose_name='AI评语')), + ('model_selection', models.CharField(default='qwen-plus', help_text='例如: qwen-plus, qwen-turbo, qwen-max', max_length=50, verbose_name='模型选择')), + ('prompt', models.TextField(default='你是一个专业的评分助手。请根据提供的转写内容,对内容质量、逻辑清晰度、语言表达等方面进行综合评分(0-100分),并给出详细的评语。请以JSON格式返回,包含"score"和"evaluation"字段。', help_text='用于指导AI评分的提示词', verbose_name='评分提示词')), + ('raw_response', models.JSONField(blank=True, help_text='大模型返回的完整JSON', null=True, verbose_name='原始响应')), + ('reasoning', models.TextField(blank=True, help_text='AI的推理过程(如果有)', null=True, verbose_name='推理过程')), + ('status', models.CharField(choices=[('PENDING', '等待中'), ('PROCESSING', '生成中'), ('COMPLETED', '已完成'), ('FAILED', '失败')], default='PENDING', max_length=20, verbose_name='评估状态')), + ('error_message', models.TextField(blank=True, null=True, verbose_name='错误信息')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('task', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='ai_evaluation', to='ai_services.transcriptiontask', verbose_name='关联任务')), + ], + options={ + 'verbose_name': 'AI智能评估', + 'verbose_name_plural': 'AI智能评估', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/ai_services/models.py b/backend/ai_services/models.py index 5977247..06d7ee4 100644 --- a/backend/ai_services/models.py +++ b/backend/ai_services/models.py @@ -25,8 +25,11 @@ class TranscriptionTask(models.Model): transcription = models.TextField(verbose_name=_('逐字稿'), blank=True, null=True) summary = models.TextField(verbose_name=_('AI总结'), blank=True, null=True) - score = models.IntegerField(verbose_name=_('AI评分'), blank=True, null=True, help_text=_('基于转写内容的评分')) - evaluation = models.TextField(verbose_name=_('AI评语'), blank=True, null=True) + + # 已解耦到 AIEvaluation 模型 + # score = models.IntegerField(verbose_name=_('AI评分'), blank=True, null=True, help_text=_('基于转写内容的评分')) + # evaluation = models.TextField(verbose_name=_('AI评语'), blank=True, null=True) + error_message = models.TextField(verbose_name=_('错误信息'), blank=True, null=True) created_at = models.DateTimeField(verbose_name=_('创建时间'), auto_now_add=True) updated_at = models.DateTimeField(verbose_name=_('更新时间'), auto_now=True) @@ -38,3 +41,58 @@ class TranscriptionTask(models.Model): def __str__(self): return f"{self.id} - {self.get_status_display()}" + + +class AIEvaluation(models.Model): + class Status(models.TextChoices): + PENDING = 'PENDING', _('等待中') + PROCESSING = 'PROCESSING', _('生成中') + COMPLETED = 'COMPLETED', _('已完成') + FAILED = 'FAILED', _('失败') + + task = models.OneToOneField( + TranscriptionTask, + on_delete=models.CASCADE, + related_name='ai_evaluation', + verbose_name=_('关联任务') + ) + + # 评分与评语 + score = models.IntegerField(verbose_name=_('AI评分'), blank=True, null=True, help_text=_('0-100分')) + evaluation = models.TextField(verbose_name=_('AI评语'), blank=True, null=True) + + # 配置选项 (可在Admin中设置) + model_selection = models.CharField( + verbose_name=_('模型选择'), + max_length=50, + default='qwen-plus', + help_text=_('例如: qwen-plus, qwen-turbo, qwen-max') + ) + prompt = models.TextField( + verbose_name=_('评分提示词'), + default='你是一个专业的评分助手。请根据提供的转写内容,对内容质量、逻辑清晰度、语言表达等方面进行综合评分(0-100分),并给出详细的评语。请以JSON格式返回,包含"score"和"evaluation"字段。', + help_text=_('用于指导AI评分的提示词') + ) + + # 原始数据与推理 + raw_response = models.JSONField(verbose_name=_('原始响应'), blank=True, null=True, help_text=_('大模型返回的完整JSON')) + reasoning = models.TextField(verbose_name=_('推理过程'), blank=True, null=True, help_text=_('AI的推理过程(如果有)')) + + status = models.CharField( + verbose_name=_('评估状态'), + max_length=20, + choices=Status.choices, + default=Status.PENDING + ) + error_message = models.TextField(verbose_name=_('错误信息'), blank=True, null=True) + + created_at = models.DateTimeField(verbose_name=_('创建时间'), auto_now_add=True) + updated_at = models.DateTimeField(verbose_name=_('更新时间'), auto_now=True) + + class Meta: + verbose_name = _('AI智能评估') + verbose_name_plural = _('AI智能评估') + ordering = ['-created_at'] + + def __str__(self): + return f"Evaluation for Task {self.task.id}" diff --git a/backend/ai_services/serializers.py b/backend/ai_services/serializers.py index d279284..c4074c0 100644 --- a/backend/ai_services/serializers.py +++ b/backend/ai_services/serializers.py @@ -1,11 +1,18 @@ from rest_framework import serializers -from .models import TranscriptionTask +from .models import TranscriptionTask, AIEvaluation + +class AIEvaluationSerializer(serializers.ModelSerializer): + class Meta: + model = AIEvaluation + fields = ['id', 'score', 'evaluation', 'model_selection', 'prompt', 'reasoning', 'status', 'error_message', 'created_at', 'updated_at'] class TranscriptionTaskSerializer(serializers.ModelSerializer): + ai_evaluation = AIEvaluationSerializer(read_only=True) + class Meta: model = TranscriptionTask - fields = ['id', 'file_url', 'task_id', 'status', 'transcription', 'summary', 'error_message', 'created_at', 'updated_at', 'score', 'evaluation', 'transcription_data', 'summary_data', 'auto_chapters_data'] - read_only_fields = ['id', 'file_url', 'task_id', 'status', 'transcription', 'summary', 'error_message', 'created_at', 'updated_at', 'score', 'evaluation', 'transcription_data', 'summary_data', 'auto_chapters_data'] + fields = ['id', 'file_url', 'task_id', 'status', 'transcription', 'summary', 'error_message', 'created_at', 'updated_at', 'transcription_data', 'summary_data', 'auto_chapters_data', 'ai_evaluation'] + read_only_fields = ['id', 'file_url', 'task_id', 'status', 'transcription', 'summary', 'error_message', 'created_at', 'updated_at', 'transcription_data', 'summary_data', 'auto_chapters_data', 'ai_evaluation'] class TranscriptionUploadSerializer(serializers.Serializer): file = serializers.FileField(help_text="上传的音频文件") diff --git a/backend/ai_services/services.py b/backend/ai_services/services.py index 3237c3b..8c95f1a 100644 --- a/backend/ai_services/services.py +++ b/backend/ai_services/services.py @@ -291,7 +291,30 @@ class AliyunTingwuService: # 保存原始数据 task.auto_chapters_data = auto_chapters - # (可选) 将章节信息追加到 summary 或 evaluation 中,或者仅保存 raw data - # 根据用户需求,这里主要保存到 model 的 auto_chapters_data 字段 (已在 models.py 定义) + # 将章节信息追加到 summary + if auto_chapters and isinstance(auto_chapters, list): + if summary_text: + summary_text.append("\n\n### 章节速览") + else: + summary_text.append("### 章节速览") + + for chapter in auto_chapters: + headline = chapter.get('Headline', '') + summary = chapter.get('Summary', '') + start_time = chapter.get('Start', 0) + + # 格式化时间戳 (毫秒 -> HH:MM:SS) + seconds = int(start_time / 1000) + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + time_str = f"{h:02d}:{m:02d}:{s:02d}" + + chapter_text = f"- [{time_str}] {headline}" + if summary: + chapter_text += f"\n {summary}" + summary_text.append(chapter_text) + + if summary_text: + task.summary = "\n".join(summary_text) task.save() diff --git a/backend/ai_services/views.py b/backend/ai_services/views.py index 0be4ca4..98f1ab2 100644 --- a/backend/ai_services/views.py +++ b/backend/ai_services/views.py @@ -7,8 +7,8 @@ from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.permissions import AllowAny from django.conf import settings from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes -from .models import TranscriptionTask -from .serializers import TranscriptionTaskSerializer, TranscriptionUploadSerializer +from .models import TranscriptionTask, AIEvaluation +from .serializers import TranscriptionTaskSerializer, TranscriptionUploadSerializer, AIEvaluationSerializer from .services import AliyunTingwuService logger = logging.getLogger(__name__) @@ -143,6 +143,51 @@ class TranscriptionTaskViewSet(viewsets.ModelViewSet): logger.error(f"处理上传请求失败: {e}") return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=True, methods=['post']) + @extend_schema( + request={ + 'application/json': { + 'type': 'object', + 'properties': { + 'model_selection': {'type': 'string', 'description': '模型选择'}, + 'prompt': {'type': 'string', 'description': '评分提示词'}, + } + } + }, + responses={200: AIEvaluationSerializer} + ) + def evaluate(self, request, pk=None): + """ + 触发AI评估 + """ + task = self.get_object() + + # 1. 检查或创建 Evaluation 对象 + evaluation, created = AIEvaluation.objects.get_or_create(task=task) + + # 2. 如果请求中有配置,更新配置 + model_selection = request.data.get('model_selection') + prompt = request.data.get('prompt') + + updated = False + if model_selection: + evaluation.model_selection = model_selection + updated = True + if prompt: + evaluation.prompt = prompt + updated = True + + if updated: + evaluation.save() + + # 3. 调用 Service 执行评估 + from .bailian_service import BailianService + service = BailianService() + service.evaluate_task(evaluation) + + serializer = AIEvaluationSerializer(evaluation) + return Response(serializer.data) + @action(detail=True, methods=['get']) @extend_schema( parameters=[ diff --git a/backend/config/settings.py b/backend/config/settings.py index 19448de..a50cabc 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -414,3 +414,5 @@ ALIYUN_OSS_BUCKET_NAME = os.environ.get('ALIYUN_OSS_BUCKET_NAME', '') ALIYUN_OSS_ENDPOINT = os.environ.get('ALIYUN_OSS_ENDPOINT', 'oss-cn-shanghai.aliyuncs.com') ALIYUN_OSS_INTERNAL_ENDPOINT = os.environ.get('ALIYUN_OSS_INTERNAL_ENDPOINT', '') ALIYUN_TINGWU_APP_KEY = os.environ.get('ALIYUN_TINGWU_APP_KEY', '') # 听悟AppKey + +DASHSCOPE_API_KEY = os.environ.get('DASHSCOPE_API_KEY', '') diff --git a/backend/requirements.txt b/backend/requirements.txt index 0f57cfa..a07d93d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,3 +28,4 @@ aliyun-python-sdk-core==2.16.0 aliyun-python-sdk-tingwu==1.0.7 oss2==2.19.1 python-dotenv +openai