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 # 准备章节/时间戳数据以辅助分析发言节奏 chapter_context = "" if task.auto_chapters_data: try: chapters_str = "" # 处理特定的 AutoChapters 结构 # 格式: {"AutoChapters": [{"Id": 1, "Start": 740, "End": 203436, "Headline": "...", "Summary": "..."}, ...]} if isinstance(task.auto_chapters_data, dict) and 'AutoChapters' in task.auto_chapters_data: chapters = task.auto_chapters_data['AutoChapters'] if isinstance(chapters, list): chapter_lines = [] for ch in chapters: # 毫秒转 MM:SS start_ms = ch.get('Start', 0) end_ms = ch.get('End', 0) start_str = f"{start_ms // 60000:02d}:{(start_ms // 1000) % 60:02d}" end_str = f"{end_ms // 60000:02d}:{(end_ms // 1000) % 60:02d}" headline = ch.get('Headline', '无标题') summary = ch.get('Summary', '') line = f"- [{start_str} - {end_str}] {headline}" if summary: line += f"\n 摘要: {summary}" chapter_lines.append(line) chapters_str = "\n".join(chapter_lines) # 如果上面的解析为空(或者格式不匹配),回退到通用 JSON dump if not chapters_str: if isinstance(task.auto_chapters_data, (dict, list)): chapters_str = json.dumps(task.auto_chapters_data, ensure_ascii=False, indent=2) else: chapters_str = str(task.auto_chapters_data) chapter_context = f"\n\n【章节与时间戳信息】\n{chapters_str}\n\n(提示:请结合上述章节时间戳信息,分析发言者的语速、节奏变化及停顿情况。)" except Exception as e: logger.warning(f"Failed to process auto_chapters_data: {e}") # 截断过长的内容以防止超出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}{chapter_context}"} ] # 增加重试机制 (最多重试3次) completion = None last_error = None import time for attempt in range(3): try: completion = self.client.chat.completions.create( model=evaluation.model_selection, messages=messages, response_format={"type": "json_object"} ) break # 成功则跳出循环 except Exception as e: last_error = e logger.warning(f"AI Evaluation attempt {attempt+1}/3 failed for eval {evaluation.id}: {e}") if attempt < 2: time.sleep(2 * (attempt + 1)) # 简单的指数退避 if not completion: raise last_error or Exception("AI Service call failed after retries") 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() # 同步结果到参赛项目 (如果关联了) self._sync_evaluation_to_project(evaluation) 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 def _sync_evaluation_to_project(self, evaluation: AIEvaluation): """ 将AI评估结果同步到关联的参赛项目(评分和评语) """ try: task = evaluation.task if not task.project: return project = task.project competition = project.competition # 1. 确定评委身份 (Based on Template) # 用户要求:评委显示的是模板名称 template_name = evaluation.template.name if evaluation.template else "AI智能评委" # 使用固定前缀 + template_id 确保唯一性,这样同一个模板在不同项目里是同一个评委 openid = f"ai_judge_{evaluation.template.id}" if evaluation.template else "ai_judge_default" # 延迟导入以避免循环依赖 from shop.models import WeChatUser from competition.models import CompetitionEnrollment, Score, Comment, ScoreDimension # 获取或创建虚拟评委用户 user, created = WeChatUser.objects.get_or_create( openid=openid, defaults={ 'nickname': template_name, 'avatar_url': 'https://ui-avatars.com/api/?name=AI&background=random&color=fff' } ) # 如果名字不匹配(比如模板改名了),更新它 if user.nickname != template_name: user.nickname = template_name user.save(update_fields=['nickname']) # 2. 确保评委已报名 (Enrollment) enrollment, _ = CompetitionEnrollment.objects.get_or_create( competition=competition, user=user, defaults={ 'role': 'judge', 'status': 'approved' } ) # 3. 同步评分 (Score) if evaluation.score is not None: # 尝试找到匹配的维度 dimensions = competition.score_dimensions.all() target_dimension = None # 0. 优先使用模板配置的维度 if evaluation.template and evaluation.template.score_dimension: # 检查配置的维度是否属于当前比赛 if evaluation.template.score_dimension.competition_id == competition.id: target_dimension = evaluation.template.score_dimension else: # 如果不属于当前比赛(跨比赛复用模板),尝试查找同名维度 target_dimension = dimensions.filter(name=evaluation.template.score_dimension.name).first() # 1. 如果未配置或未找到,尝试匹配 "AI Rating" (用户指定默认值) if not target_dimension: target_dimension = dimensions.filter(name__iexact="AI Rating").first() # 2. 尝试匹配包含 "AI" 的维度 if not target_dimension: for dim in dimensions: if "AI" in dim.name.upper(): target_dimension = dim break # 3. 尝试匹配模板名称 if not target_dimension: target_dimension = dimensions.filter(name=template_name).first() # 4. 最后兜底:使用第一个维度 if not target_dimension and dimensions.exists(): target_dimension = dimensions.first() if target_dimension: Score.objects.update_or_create( project=project, judge=enrollment, dimension=target_dimension, defaults={'score': evaluation.score} ) logger.info(f"Synced AI score {evaluation.score} to project {project.id} dimension {target_dimension.name}") # 4. 同步评语 (Comment) if evaluation.evaluation: # 检查是否已存在该评委的评语,避免重复 comment = Comment.objects.filter(project=project, judge=enrollment).first() if comment: comment.content = evaluation.evaluation comment.save() else: Comment.objects.create( project=project, judge=enrollment, content=evaluation.evaluation ) logger.info(f"Synced AI comment to project {project.id}") except Exception as e: logger.error(f"Failed to sync evaluation to project: {e}") def summarize_task(self, task): """ 对转写任务进行总结 """ if not self.client: logger.warning("BailianService not initialized, skipping summary.") return if not task.transcription: logger.warning(f"Task {task.id} has no transcription, skipping summary.") return try: content = task.transcription # 简单截断防止过长 if len(content) > 15000: content = content[:15000] + "...(内容过长已截断)" # 准备上下文数据 context_data = "" if task.summary_data: context_data += f"\n\n【总结原始数据】\n{json.dumps(task.summary_data, ensure_ascii=False, indent=2)}" if task.auto_chapters_data: context_data += f"\n\n【章节原始数据】\n{json.dumps(task.auto_chapters_data, ensure_ascii=False, indent=2)}" system_prompt = f"""你是一个专业的会议/内容总结助手。请根据提供的【转写文本】、【总结原始数据】和【章节原始数据】,生成一份结构清晰、内容详实的总结报告。 请按照以下结构输出(Markdown格式): 1. **标题**:基于内容生成一个合适的标题。 2. **核心摘要**:简要概括主要内容。 3. **主要观点/话题**:结合思维导图数据,列出关键话题和层级。 4. **章节速览**:结合章节数据,列出时间点和主要内容。[HH:MM:SS]格式来把章节列出来 5. **问答精选**(如果有):基于问答总结数据,列出重要问答。 请确保语言通顺,重点突出,能够还原内容的逻辑结构。""" user_content = f"以下是需要总结的内容:\n\n【转写文本】\n{content}{context_data}" messages = [ {'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': user_content} ] # 使用 qwen-plus 作为默认模型 completion = self.client.chat.completions.create( model="qwen-plus", messages=messages ) summary_content = completion.choices[0].message.content task.summary = summary_content task.save(update_fields=['summary']) logger.info(f"Task {task.id} summary generated successfully.") except Exception as e: logger.error(f"Failed to generate summary for task {task.id}: {e}")