|
@@ -30,6 +30,22 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
userAnswer = '';
|
|
|
recognition: any;
|
|
|
interimTranscript = '';
|
|
|
+ private voiceEndTimer: any;
|
|
|
+ private readonly VOICE_END_DELAY = 1500;
|
|
|
+ private speechBuffer: string[] = [];
|
|
|
+ private lastFinalResultIndex = 0;
|
|
|
+
|
|
|
+ // 思考时间控制
|
|
|
+ private silenceTimer: any;
|
|
|
+ silenceDuration = 0;
|
|
|
+ readonly MAX_SILENCE = 5;
|
|
|
+
|
|
|
+ // 自我介绍相关
|
|
|
+ private isSelfIntroduction = false;
|
|
|
+ private minSelfIntroDuration = 5000; // 至少5秒自我介绍时间
|
|
|
+ private selfIntroTimer: any;
|
|
|
+ private minSelfIntroWords = 30; // 至少30字的自我介绍
|
|
|
+
|
|
|
// 问题列表
|
|
|
mainQuestions = [
|
|
|
"欢迎参加本次AI面试,我是您的面试官AI助手。",
|
|
@@ -45,7 +61,7 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
messages: {sender: 'ai' | 'user', text: string}[] = [];
|
|
|
|
|
|
// 头像状态
|
|
|
- avatarImage = "assets/default-avatar.svg";
|
|
|
+ avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f916.svg";
|
|
|
expressionText = "等待开始...";
|
|
|
|
|
|
// 评估指标
|
|
@@ -59,7 +75,6 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
private radarChart: any;
|
|
|
|
|
|
constructor(private aiService: AiApiService) {
|
|
|
- // 初始化语音识别
|
|
|
this.initSpeechRecognition();
|
|
|
}
|
|
|
|
|
@@ -72,52 +87,128 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.initRadarChart();
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
private initSpeechRecognition() {
|
|
|
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
|
|
if (SpeechRecognition) {
|
|
|
this.recognition = new SpeechRecognition();
|
|
|
this.recognition.lang = 'zh-CN';
|
|
|
- this.recognition.interimResults = true; // 启用临时结果
|
|
|
+ this.recognition.interimResults = true;
|
|
|
this.recognition.continuous = true;
|
|
|
|
|
|
+ this.recognition.onsoundstart = () => {
|
|
|
+ this.updateAvatarState('listening');
|
|
|
+ this.resetSilenceTimer();
|
|
|
+ };
|
|
|
+
|
|
|
+ this.recognition.onsoundend = () => {
|
|
|
+ this.stopSilenceTimer();
|
|
|
+ };
|
|
|
+
|
|
|
this.recognition.onresult = (event: any) => {
|
|
|
+ this.resetSilenceTimer();
|
|
|
+
|
|
|
let finalTranscript = '';
|
|
|
let interimTranscript = '';
|
|
|
+ let hasNewFinalResult = false;
|
|
|
|
|
|
- for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
|
+ for (let i = this.lastFinalResultIndex; i < event.results.length; i++) {
|
|
|
const transcript = event.results[i][0].transcript;
|
|
|
if (event.results[i].isFinal) {
|
|
|
+ this.speechBuffer.push(transcript);
|
|
|
finalTranscript += transcript;
|
|
|
+ hasNewFinalResult = true;
|
|
|
+ this.lastFinalResultIndex = i + 1;
|
|
|
} else {
|
|
|
- interimTranscript += transcript;
|
|
|
+ interimTranscript = transcript;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (finalTranscript) {
|
|
|
- this.userAnswer = finalTranscript;
|
|
|
- this.showSubmitButton = true;
|
|
|
- this.showCancelButton = true;
|
|
|
+ if (this.speechBuffer.length > 0) {
|
|
|
+ this.userAnswer = this.speechBuffer.join(' ');
|
|
|
}
|
|
|
+
|
|
|
+ if (hasNewFinalResult) {
|
|
|
+ this.resetVoiceEndTimer();
|
|
|
+ }
|
|
|
+
|
|
|
this.interimTranscript = interimTranscript;
|
|
|
};
|
|
|
|
|
|
this.recognition.onerror = (event: any) => {
|
|
|
console.error('语音识别错误:', event.error);
|
|
|
this.updateAvatarState('listening');
|
|
|
+ this.stopVoiceEndTimer();
|
|
|
+
|
|
|
+ if (this.isListening && event.error !== 'no-speech') {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.isListening) {
|
|
|
+ this.recognition.start();
|
|
|
+ }
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
this.recognition.onend = () => {
|
|
|
if (this.isListening) {
|
|
|
- this.stopVoiceInput();
|
|
|
+ this.recognition.start();
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private resetVoiceEndTimer() {
|
|
|
+ this.stopVoiceEndTimer();
|
|
|
+ this.voiceEndTimer = setTimeout(() => {
|
|
|
+ if (this.userAnswer.trim().length > 0) {
|
|
|
+ this.showSubmitButton = true;
|
|
|
+ this.showCancelButton = true;
|
|
|
+ }
|
|
|
+ }, this.VOICE_END_DELAY);
|
|
|
+ }
|
|
|
+
|
|
|
+ private stopVoiceEndTimer() {
|
|
|
+ if (this.voiceEndTimer) {
|
|
|
+ clearTimeout(this.voiceEndTimer);
|
|
|
+ this.voiceEndTimer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private startSelfIntroTimer() {
|
|
|
+ this.stopSelfIntroTimer();
|
|
|
+ this.selfIntroTimer = setTimeout(() => {
|
|
|
+ if (this.isListening && this.userAnswer.trim().length > 0) {
|
|
|
+ this.stopVoiceInput();
|
|
|
+ this.showSubmitButton = true;
|
|
|
+ this.showCancelButton = true;
|
|
|
+ }
|
|
|
+ }, this.minSelfIntroDuration);
|
|
|
+ }
|
|
|
+
|
|
|
+ private stopSelfIntroTimer() {
|
|
|
+ if (this.selfIntroTimer) {
|
|
|
+ clearTimeout(this.selfIntroTimer);
|
|
|
+ this.selfIntroTimer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private resetSilenceTimer() {
|
|
|
+ this.stopSilenceTimer();
|
|
|
+ this.silenceDuration = 0;
|
|
|
+
|
|
|
+ this.silenceTimer = setInterval(() => {
|
|
|
+ this.silenceDuration++;
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private stopSilenceTimer() {
|
|
|
+ if (this.silenceTimer) {
|
|
|
+ clearInterval(this.silenceTimer);
|
|
|
+ this.silenceTimer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private initProgressTimer(): void {
|
|
|
- const totalSeconds = 10 * 60; // 10分钟 = 600秒
|
|
|
+ const totalSeconds = 10 * 60;
|
|
|
let elapsedSeconds = 0;
|
|
|
|
|
|
const timer = setInterval(() => {
|
|
@@ -129,19 +220,17 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
elapsedSeconds++;
|
|
|
this.progress = Math.min((elapsedSeconds / totalSeconds) * 100, 100);
|
|
|
|
|
|
- // 更新时间显示
|
|
|
const remainingSeconds = totalSeconds - elapsedSeconds;
|
|
|
const minutes = Math.floor(remainingSeconds / 60);
|
|
|
const seconds = remainingSeconds % 60;
|
|
|
this.remainingTime = `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
|
|
|
|
|
|
- // 时间到自动结束
|
|
|
if (elapsedSeconds >= totalSeconds) {
|
|
|
clearInterval(timer);
|
|
|
this.addAIMessage("时间到,本次面试结束");
|
|
|
this.interviewEnded = true;
|
|
|
}
|
|
|
- }, 1000); // 改为每秒更新一次
|
|
|
+ }, 1000);
|
|
|
}
|
|
|
|
|
|
initConversation(): void {
|
|
@@ -160,10 +249,20 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.currentQuestionIndex = index;
|
|
|
this.isFollowUpQuestion = false;
|
|
|
this.currentQuestion = this.mainQuestions[index];
|
|
|
+ this.isSelfIntroduction = index === 1; // 第二个问题是自我介绍
|
|
|
+
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.userAnswer = '';
|
|
|
+ this.interimTranscript = '';
|
|
|
|
|
|
this.addAIMessage(this.currentQuestion);
|
|
|
this.updateAvatarState('speaking');
|
|
|
- this.speakText(this.currentQuestion); // 自动语音播放问题
|
|
|
+ this.speakText(this.currentQuestion);
|
|
|
+
|
|
|
+ if (this.isSelfIntroduction) {
|
|
|
+ this.startSelfIntroTimer();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async askFollowUpQuestion(followUpText: string): Promise<void> {
|
|
@@ -171,6 +270,12 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
|
|
|
this.isFollowUpQuestion = true;
|
|
|
this.currentQuestion = followUpText;
|
|
|
+ this.isSelfIntroduction = false;
|
|
|
+
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.userAnswer = '';
|
|
|
+ this.interimTranscript = '';
|
|
|
|
|
|
this.addAIMessage("关于您刚才的回答,我有一个跟进问题...");
|
|
|
setTimeout(() => {
|
|
@@ -192,6 +297,10 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.messages = [];
|
|
|
this.metrics = { expressiveness: 0, professionalism: 0, relevance: 0 };
|
|
|
this.progress = 0;
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.isSelfIntroduction = false;
|
|
|
+ this.stopSelfIntroTimer();
|
|
|
this.initConversation();
|
|
|
}
|
|
|
|
|
@@ -243,21 +352,82 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
|
|
|
this.isListening = true;
|
|
|
this.userAnswer = '';
|
|
|
- this.updateAvatarState('listening');
|
|
|
- this.recognition.start();
|
|
|
+ this.interimTranscript = '';
|
|
|
+ this.showSubmitButton = false;
|
|
|
+ this.showCancelButton = false;
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+
|
|
|
+ // 调整自我介绍问题的识别参数
|
|
|
+ if (this.isSelfIntroduction) {
|
|
|
+ this.recognition.continuous = true;
|
|
|
+ this.recognition.interimResults = true;
|
|
|
+ this.recognition.maxAlternatives = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.recognition.start();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('语音识别启动失败:', e);
|
|
|
+ setTimeout(() => this.startVoiceInput(), 500);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
stopVoiceInput(): void {
|
|
|
+ if (!this.isListening) return;
|
|
|
+
|
|
|
this.isListening = false;
|
|
|
- if (this.recognition) {
|
|
|
+ this.stopSilenceTimer();
|
|
|
+ this.stopVoiceEndTimer();
|
|
|
+ this.stopSelfIntroTimer();
|
|
|
+
|
|
|
+ try {
|
|
|
this.recognition.stop();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('语音识别停止失败:', e);
|
|
|
}
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.userAnswer.trim().length > 0) {
|
|
|
+ this.showSubmitButton = true;
|
|
|
+ this.showCancelButton = true;
|
|
|
+ }
|
|
|
+ }, 500);
|
|
|
+
|
|
|
this.updateAvatarState('waiting');
|
|
|
}
|
|
|
|
|
|
+ private getWordCount(text: string): number {
|
|
|
+ // 简单的中文字数统计
|
|
|
+ return text.replace(/[^\u4e00-\u9fa5]/g, '').length;
|
|
|
+ }
|
|
|
+
|
|
|
+ private validateSelfIntroduction(): boolean {
|
|
|
+ if (!this.isSelfIntroduction) return true;
|
|
|
+
|
|
|
+ const wordCount = this.getWordCount(this.userAnswer);
|
|
|
+ if (wordCount < this.minSelfIntroWords) {
|
|
|
+ this.addAIMessage(`您的自我介绍需要至少${this.minSelfIntroWords}字,请再详细介绍一下。`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
async submitAnswer(): Promise<void> {
|
|
|
if (!this.userAnswer || this.isAnalyzing) return;
|
|
|
|
|
|
+ // 验证自我介绍是否符合要求
|
|
|
+ if (!this.validateSelfIntroduction()) {
|
|
|
+ // 不符合要求,重置状态让用户重新回答
|
|
|
+ this.userAnswer = '';
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.showSubmitButton = false;
|
|
|
+ this.showCancelButton = false;
|
|
|
+ this.startVoiceInput();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
this.showSubmitButton = false;
|
|
|
this.showCancelButton = false;
|
|
|
this.addUserMessage(this.userAnswer);
|
|
@@ -271,31 +441,25 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.userAnswer
|
|
|
);
|
|
|
|
|
|
- // 更新评估指标
|
|
|
this.metrics = {
|
|
|
expressiveness: evaluation.metrics.expressiveness,
|
|
|
professionalism: evaluation.metrics.professionalism,
|
|
|
relevance: evaluation.metrics.relevance
|
|
|
};
|
|
|
|
|
|
- // 添加AI反馈
|
|
|
this.addAIMessage(evaluation.feedback);
|
|
|
|
|
|
- // 检查回答相关性
|
|
|
if (evaluation.metrics.relevance < 30) {
|
|
|
this.addAIMessage("您的回答与问题相关性较低,本次面试结束。");
|
|
|
this.interviewEnded = true;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 处理问题流程
|
|
|
if (!this.isFollowUpQuestion && this.currentQuestionIndex !== 1) {
|
|
|
- // 主问题(非自我介绍)需要追问
|
|
|
setTimeout(() => {
|
|
|
this.askFollowUpQuestion(evaluation.followUpQuestion);
|
|
|
}, 1500);
|
|
|
} else {
|
|
|
- // 进入下一个问题
|
|
|
setTimeout(() => {
|
|
|
this.askQuestion(this.currentQuestionIndex + 1);
|
|
|
}, 1500);
|
|
@@ -307,6 +471,9 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
} finally {
|
|
|
this.isAnalyzing = false;
|
|
|
this.userAnswer = '';
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.isSelfIntroduction = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -314,6 +481,9 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.showSubmitButton = false;
|
|
|
this.showCancelButton = false;
|
|
|
this.userAnswer = '';
|
|
|
+ this.speechBuffer = [];
|
|
|
+ this.lastFinalResultIndex = 0;
|
|
|
+ this.isSelfIntroduction = false;
|
|
|
this.updateAvatarState('listening');
|
|
|
}
|
|
|
|
|
@@ -324,27 +494,24 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
}
|
|
|
|
|
|
updateAvatarState(state: 'speaking' | 'listening' | 'waiting' | 'analyzing' | 'default'): void {
|
|
|
- switch(state) {
|
|
|
- case 'speaking':
|
|
|
- this.avatarImage = "assets/ai-speaking.svg";
|
|
|
- this.expressionText = "正在提问...";
|
|
|
- break;
|
|
|
- case 'listening':
|
|
|
- this.avatarImage = "assets/ai-listening.svg";
|
|
|
- this.expressionText = "正在聆听...";
|
|
|
- break;
|
|
|
- case 'waiting':
|
|
|
- this.avatarImage = "assets/ai-waiting.svg";
|
|
|
- this.expressionText = "等待确认...";
|
|
|
- break;
|
|
|
- case 'analyzing':
|
|
|
- this.avatarImage = "assets/ai-analyzing.svg";
|
|
|
- this.expressionText = "分析回答中...";
|
|
|
- break;
|
|
|
- default:
|
|
|
- this.avatarImage = "assets/default-avatar.svg";
|
|
|
- this.expressionText = "AI面试官";
|
|
|
- }
|
|
|
+ const emojiMap = {
|
|
|
+ 'speaking': '1f5e3',
|
|
|
+ 'listening': '1f3a4',
|
|
|
+ 'waiting': '1f914',
|
|
|
+ 'analyzing': '1f9d0',
|
|
|
+ 'default': '1f916'
|
|
|
+ };
|
|
|
+
|
|
|
+ const stateTexts = {
|
|
|
+ 'speaking': "正在提问...",
|
|
|
+ 'listening': "正在聆听...",
|
|
|
+ 'waiting': "等待确认...",
|
|
|
+ 'analyzing': "分析回答中...",
|
|
|
+ 'default': "AI面试官"
|
|
|
+ };
|
|
|
+
|
|
|
+ this.avatarImage = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${emojiMap[state]}.svg`;
|
|
|
+ this.expressionText = stateTexts[state];
|
|
|
}
|
|
|
|
|
|
toggleRadarChart(): void {
|
|
@@ -421,6 +588,4 @@ export class PageInterview implements OnInit, AfterViewInit {
|
|
|
this.radarChart?.resize();
|
|
|
});
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
}
|