feat: 升级剧本评分系统为 DeepSeek V4 多维评分体系

变更内容:
- scriptEvalClient: 替换系统提示词为 DeepSeek V4 完整版(六维评分细则、
  评分类别、评分铁律、等级标准),维度满分调整为 hook 20/plot 20/
  character 18/dialogue 15/visual 15/content 12,总分算法改为直接累加,
  模型使用 qwen3.7-max,增加请求链路 console 日志
- ScriptTokensPage: 同步维度满分和描述(角色塑造 18、内容深度 12),
  增加页面挂载和评测流程的 console 日志
- vite.config: esbuild.drop 移除 "console",保留 "debugger",
  确保开发环境 console 日志正常输出
This commit is contained in:
2026-06-02 16:04:26 +08:00
parent bedee3ba8d
commit d1b5d64bc8
3 changed files with 68 additions and 31 deletions
+44 -23
View File
@@ -10,39 +10,50 @@ export interface ScriptEvalResult {
suggestions: string[];
}
const EVAL_SYSTEM_PROMPT = `你是一位专业的剧本评专家。请对用户提供的剧本进行六维评分分析,并以严格的 JSON 格式返回结果
const EVAL_SYSTEM_PROMPT = `你是一位资深影视剧本评专家,拥有二十年以上的编剧、制片和剧本医生经验。你精通各类影视叙事理论(三幕式、英雄之旅、起承转合、序列法),同时深度跟踪AIGC短剧/漫剧行业最新趋势。你的任务是对用户提供的剧本进行严谨、系统、多维度的量化评分
六个评分维度:
1. hook(钩子设计,满分20):开篇吸引力、悬念设置、黄金三秒法则
2. character(角色塑造,满分15):人物立体度、动机合理性、弧光设计
3. plot(剧情结构,满分20):起承转合、节奏把控、冲突设计
4. dialogue(台词对白,满分15):语言质感、角色差异化、潜台词
5. visual(画面表现,满分15):镜头感、空间层次、视觉冲击力
6. content(内容深度,满分15):主题表达、情感共鸣、思想内核
【剧本类型识别】
收到剧本后,首先判断类型:AIGC短剧/漫剧(单集5-30分钟,竖屏平台,高密度反转、强节奏)或传统影视剧本(单集40分钟以上,长视频平台,完整起承转合)。类型判定将影响各维度的评价侧重点。
请严格按以下 JSON 格式返回(不要包含任何其他文字):
【评分体系(100分制,六个维度)】
1. hook 钩子设计(20分):开篇钩子、集末钩子、场景内钩子、悬念链完整性。短剧前3秒须有即时爆点;长剧第一幕结束前须建立核心悬念。
2. plot 剧情结构(20分):结构框架、节奏控制、冲突设计、逻辑自洽。短剧"每分钟有事件",反转密度加分;长剧需处理好B线C线与主线交织。
3. character 角色塑造(18分):主角弧光、角色辨识度、角色动机、配角质量。短剧角色须在前2分钟建立;长剧需要内在矛盾和多阶段成长。
4. dialogue 台词对白(15分):角色语言区分度、信息传递效率、潜台词与留白、金句与记忆点。
5. visual 画面表现(15分):场景描写质量、视觉叙事技巧、镜头感与节奏、制作可行性。AIGC需考虑AI生成技术边界与一致性。
6. content 内容深度(12分):主题表达、情感共鸣、社会/人性洞察。
【评分铁律】
- 扣分必须明确指出剧本中的具体段落/场景/台词。
- 严禁给出任何维度的满分,必须有扣分理由。
- 优缺点都要充分展开,不可只批不夸或只夸不批。
- 不因题材类型偏见降低评分,不因某一方面出色而抬高其他维度(避免光环效应)。
- 敢于拉开各维度分数差距,避免全部给中等分数。
【等级标准】按总分百分比:S≥90 | A 80-89 | B 70-79 | C 60-69 | D<60。
请严格按以下 JSON 格式返回(不要包含任何其他文字,不要用代码块包裹以外的说明):
{
"dimensionScores": { "hook": 数字, "character": 数字, "plot": 数字, "dialogue": 数字, "visual": 数字, "content": 数字 },
"summary": "一句话总结评价",
"issues": ["问题1", "问题2", ...],
"highlights": ["亮点1", "亮点2", ...],
"suggestions": ["建议1", "建议2", ...]
"dimensionScores": { "hook": 数字, "plot": 数字, "character": 数字, "dialogue": 数字, "visual": 数字, "content": 数字 },
"summary": "200-300字综合评价,概括整体质量、市场潜力与目标受众匹配度",
"issues": ["每条指出具体维度的扣分点并引用剧本原文位置", ...],
"highlights": ["核心亮点,引用剧本具体场景", ...],
"suggestions": ["按优先级排列的改进建议(最优先/次优先/可优化)", ...]
}`;
const DIMENSION_WEIGHTS: Record<string, { maxScore: number; weight: number }> = {
hook: { maxScore: 20, weight: 0.2 },
character: { maxScore: 15, weight: 0.15 },
plot: { maxScore: 20, weight: 0.2 },
dialogue: { maxScore: 15, weight: 0.15 },
visual: { maxScore: 15, weight: 0.15 },
content: { maxScore: 15, weight: 0.15 },
const DIMENSION_WEIGHTS: Record<string, { maxScore: number }> = {
hook: { maxScore: 20 },
plot: { maxScore: 20 },
character: { maxScore: 18 },
dialogue: { maxScore: 15 },
visual: { maxScore: 15 },
content: { maxScore: 12 },
};
function computeTotalAndGrade(scores: Record<string, number>): { totalScore: number; grade: string } {
const totalScore = Math.round(
Object.entries(DIMENSION_WEIGHTS).reduce((sum, [key, dim]) => {
const score = Math.max(0, Math.min(dim.maxScore, scores[key] ?? 0));
return sum + (score / dim.maxScore) * 100 * dim.weight;
return sum + Math.max(0, Math.min(dim.maxScore, scores[key] ?? 0));
}, 0),
);
const grade = totalScore >= 90 ? "S" : totalScore >= 80 ? "A" : totalScore >= 70 ? "B" : totalScore >= 60 ? "C" : "D";
@@ -56,6 +67,7 @@ function extractJson(text: string): unknown {
}
export async function evaluateScript(script: string, signal?: AbortSignal): Promise<ScriptEvalResult> {
console.log("[API] 发送评测请求,剧本长度:", script.slice(0, 8000).length, "字符");
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
@@ -71,11 +83,15 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
signal,
});
console.log("[API] 响应状态:", res.status, res.statusText);
if (!res.ok) {
throw new Error(`评测请求失败 (${res.status})`);
}
const payload = await res.json();
console.log("[API] 原始响应体:", payload);
const content: string = payload?.choices?.[0]?.message?.content
?? payload?.result?.content
?? payload?.content
@@ -84,7 +100,11 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
if (!content) throw new Error("模型未返回有效内容");
console.log("[API] 模型返回内容 (前500字符):", content.slice(0, 500));
const parsed = extractJson(content) as Record<string, unknown>;
console.log("[API] 解析后的JSON:", parsed);
const dimensionScores: Record<string, number> = {};
const rawScores = parsed.dimensionScores as Record<string, number> | undefined;
if (!rawScores || typeof rawScores !== "object") throw new Error("评分格式异常");
@@ -95,6 +115,7 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
}
const { totalScore, grade } = computeTotalAndGrade(dimensionScores);
console.log("[API] 计算后总分:", totalScore, "等级:", grade);
return {
totalScore,
@@ -1,5 +1,5 @@
import { CopyOutlined, DownOutlined, DownloadOutlined, FileTextOutlined, ReloadOutlined, TrophyOutlined, UploadOutlined } from "@ant-design/icons";
import { useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { evaluateScript } from "../../api/scriptEvalClient";
interface ScoreDimension {
@@ -35,9 +35,9 @@ const scoreDimensions: ScoreDimension[] = [
{
key: "character",
label: "角色塑造",
maxScore: 15,
weight: 0.15,
description: "人物立体度、动机合理性、弧光设计",
maxScore: 18,
weight: 0.18,
description: "主角弧光、角色辨识度、动机、配角质量",
},
{
key: "plot",
@@ -63,9 +63,9 @@ const scoreDimensions: ScoreDimension[] = [
{
key: "content",
label: "内容深度",
maxScore: 15,
weight: 0.15,
description: "主题表达、情感共鸣、思想内核",
maxScore: 12,
weight: 0.12,
description: "主题表达、情感共鸣、社会/人性洞察",
},
];
@@ -170,6 +170,10 @@ function ScriptTokensPage() {
const [copied, setCopied] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
console.log("[剧本评分] 页面已加载,ScriptTokensPage mounted");
}, []);
const hasContent = Boolean(script.trim());
const lineNumbers = useMemo(() => {
const count = Math.min(160, Math.max(10, script.split(/\r\n|\r|\n/).length));
@@ -202,14 +206,26 @@ function ScriptTokensPage() {
};
const handleEvaluate = async () => {
console.log("[剧本评测] 点击开始评测,hasContent:", hasContent, "script长度:", script.length);
if (!hasContent) return;
setLoading(true);
setResult(null);
setEvalError(null);
try {
console.log("[剧本评测] 开始评测,剧本长度:", script.length, "字符");
const aiResult = await evaluateScript(script);
console.log("[剧本评测] 评测完成,结果:", {
总分: aiResult.totalScore,
等级: aiResult.grade,
维度得分: aiResult.dimensionScores,
摘要: aiResult.summary,
亮点: aiResult.highlights,
问题: aiResult.issues,
建议: aiResult.suggestions,
});
setResult(aiResult);
} catch (err) {
console.error("[剧本评测] 评测失败:", err);
setEvalError(err instanceof Error ? err.message : "评测服务暂时不可用,请稍后重试");
}
setDetailsExpanded(true);
+1 -1
View File
@@ -22,7 +22,7 @@ export default defineConfig({
host: "127.0.0.1",
},
esbuild: {
drop: ["console", "debugger"],
drop: ["debugger"],
},
build: {
sourcemap: "hidden",