This commit is contained in:
@@ -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',),
|
||||
}),
|
||||
)
|
||||
|
||||
98
backend/ai_services/bailian_service.py
Normal file
98
backend/ai_services/bailian_service.py
Normal file
@@ -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
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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="上传的音频文件")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user