feat: 对齐 Jev 真实 API schema、Web Speech 语音输入(优雅降级)、C1-01 语音化身份登记与真 key 冒烟脚本
This commit is contained in:
+98
-5
@@ -1,7 +1,7 @@
|
||||
import type { Chapter, NarrationNode, SceneVariant, StoryNode } from './types';
|
||||
import type { Chapter, IdentityProfile, NarrationNode, SceneVariant, StoryNode } from './types';
|
||||
import type { EventStore, StoryEvent } from './eventStore';
|
||||
import { makeEvent } from './eventStore';
|
||||
import type { JevClient } from '../services/jev';
|
||||
import type { JevClient, JevQuestion } from '../services/jev';
|
||||
|
||||
export interface EngineState {
|
||||
sessionId: string;
|
||||
@@ -151,19 +151,112 @@ export class StoryEngine {
|
||||
{
|
||||
emotion_signature: {
|
||||
type: 'choice',
|
||||
description: `根据讲述文本选择最贴合的情绪签名(讲述点 ${node.pointId})`,
|
||||
options: node.variants.map((v) => v.signature),
|
||||
instructions: `根据讲述文本选择最贴合的情绪签名(讲述点 ${node.pointId})`,
|
||||
criteria: Object.fromEntries(node.variants.map((v) => [v.signature, v.label])),
|
||||
},
|
||||
},
|
||||
);
|
||||
const picked = answer.emotion_signature;
|
||||
if (picked?.type === 'choice') {
|
||||
const match = node.variants.find((v) => v.signature === picked.selected);
|
||||
const match = node.variants.find((v) => v.signature === picked.choice);
|
||||
if (match) return match;
|
||||
}
|
||||
return node.variants[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* C1-01 身份登记:从自由介绍文本中用 Jev 抽取结构化字段(匹配硬过滤字段)。
|
||||
* 抽取不出来的字段留空,由用户在确认页补填。
|
||||
*/
|
||||
async extractIdentity(transcript: string): Promise<IdentityProfile> {
|
||||
const node = this.currentNode();
|
||||
if (node.type !== 'identity') throw new Error('当前节点不是身份登记点');
|
||||
const ageBands = ['18-22', '23-27', '28-32', '33-40', '41及以上'];
|
||||
const questions: Record<string, JevQuestion> = {
|
||||
gender: {
|
||||
type: 'choice',
|
||||
instructions: '从这段自我介绍中判断说话人的性别',
|
||||
criteria: { 男: '明确为男性', 女: '明确为女性', 其他: '非二元性别', 未提及: '文本中没有可判断的信息' },
|
||||
},
|
||||
age: {
|
||||
type: 'score',
|
||||
instructions: '根据自我介绍判断说话人所属的年龄段',
|
||||
criteria: ageBands,
|
||||
},
|
||||
occupation: {
|
||||
type: 'choice',
|
||||
instructions: '从这段自我介绍中判断说话人的职业类别',
|
||||
criteria: {
|
||||
互联网: '互联网/软件/产品经理等',
|
||||
学术科研: '高校、科研院所、教师、研究员',
|
||||
金融: '银行、证券、投资等',
|
||||
体制内: '公务员、事业单位、国企',
|
||||
自由职业: '自由职业、个体、创作者',
|
||||
学生: '在校学生',
|
||||
其他: '以上都不是',
|
||||
未提及: '文本中没有可判断的信息',
|
||||
},
|
||||
},
|
||||
education: {
|
||||
type: 'choice',
|
||||
instructions: '从这段自我介绍中判断说话人的最高学历',
|
||||
criteria: {
|
||||
博士: '博士',
|
||||
硕士: '硕士(含在读)',
|
||||
本科: '本科',
|
||||
大专及以下: '大专或以下',
|
||||
未提及: '文本中没有可判断的信息',
|
||||
},
|
||||
},
|
||||
city_mentioned: {
|
||||
type: 'noul',
|
||||
instructions: '自我介绍中是否提及了说话人所在的城市',
|
||||
},
|
||||
city: {
|
||||
type: 'choice',
|
||||
instructions: '自我介绍中提到的城市是哪一个(首发城市杭州,其余为常见城市兜底)',
|
||||
criteria: {
|
||||
杭州: '杭州', 北京: '北京', 上海: '上海', 深圳: '深圳', 广州: '广州',
|
||||
南京: '南京', 成都: '成都', 西安: '西安', 武汉: '武汉',
|
||||
未提及: '没有提及,或不在以上城市中',
|
||||
},
|
||||
},
|
||||
};
|
||||
const answers = await this.jev.ask({ transcript }, questions);
|
||||
const choiceVal = (id: string): string => {
|
||||
const a = answers[id];
|
||||
return a?.type === 'choice' && a.choice !== '未提及' ? a.choice : '';
|
||||
};
|
||||
let age = '';
|
||||
const ageAnswer = answers.age;
|
||||
// 年龄段是 score 分档:置信度太低(视为未提及)则留空给用户补填
|
||||
if (ageAnswer?.type === 'score' && ageAnswer.confidence >= 0.4) {
|
||||
const idx = Math.max(0, Math.min(ageBands.length - 1, Math.round(ageAnswer.score)));
|
||||
age = ageBands[idx];
|
||||
}
|
||||
let city = choiceVal('city');
|
||||
const mentioned = answers.city_mentioned;
|
||||
if (mentioned?.type === 'noul' && mentioned.probability < 0.5) city = '';
|
||||
return { city, age, gender: choiceVal('gender'), occupation: choiceVal('occupation'), education: choiceVal('education') };
|
||||
}
|
||||
|
||||
/** C1-01 确认页通过后调用:append profile 事件并推进 */
|
||||
async confirmIdentity(profile: IdentityProfile): Promise<void> {
|
||||
const node = this.currentNode();
|
||||
if (node.type !== 'identity') throw new Error('当前节点不是身份登记点');
|
||||
await this.store.append(
|
||||
makeEvent({
|
||||
sessionId: this.state.sessionId,
|
||||
chapterId: this.state.chapterId,
|
||||
kind: 'profile',
|
||||
pointId: node.pointId,
|
||||
dimensions: node.dimensions,
|
||||
profile,
|
||||
}),
|
||||
);
|
||||
this.state.nodeId = node.next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重玩(ADR-0004):产生新轨迹(新 sessionId),不修改旧事件。
|
||||
* 默认重玩当前章,也可指定章节。
|
||||
|
||||
Reference in New Issue
Block a user