@@ -1,809 +0,0 @@
//
// KBAiMainVC . m
// keyBoard
//
// Created by Mac on 2026 / 1 / 15.
//
# import "KBAiMainVC.h"
# import "ConversationOrchestrator.h"
# import "AiVM.h"
# import "AudioSessionManager.h"
# import "DeepgramStreamingManager.h"
# import "KBAICommentView.h"
# import "KBChatTableView.h"
# import "KBAiRecordButton.h"
# import "KBHUD.h"
# import "KBChatLimitPopView.h"
# import "KBPayMainVC.h"
# import "LSTPopView.h"
# import "VoiceChatStreamingManager.h"
# import "KBUserSessionManager.h"
# import < AVFoundation / AVFoundation . h >
@ interface KBAiMainVC ( ) < KBAiRecordButtonDelegate ,
VoiceChatStreamingManagerDelegate ,
DeepgramStreamingManagerDelegate ,
AVAudioPlayerDelegate ,
KBChatLimitPopViewDelegate >
@ property ( nonatomic , weak ) LSTPopView * popView ;
@ property ( nonatomic , weak ) LSTPopView * limitPopView ;
// UI
@ property ( nonatomic , strong ) KBChatTableView * chatView ;
@ property ( nonatomic , strong ) KBAiRecordButton * recordButton ;
@ property ( nonatomic , strong ) UILabel * statusLabel ;
@ property ( nonatomic , strong ) UILabel * transcriptLabel ;
@ property ( nonatomic , strong ) UIButton * commentButton ;
@ property ( nonatomic , strong ) KBAICommentView * commentView ;
@ property ( nonatomic , strong ) UIView * tabbarBackgroundView ;
@ property ( nonatomic , strong ) UIVisualEffectView * blurEffectView ;
@ property ( nonatomic , strong ) CAGradientLayer * gradientLayer ;
@ property ( nonatomic , strong ) UIImageView * personImageView ;
// 核 心 模 块
@ property ( nonatomic , strong ) ConversationOrchestrator * orchestrator ;
@ property ( nonatomic , strong ) VoiceChatStreamingManager * streamingManager ;
@ property ( nonatomic , strong ) DeepgramStreamingManager * deepgramManager ;
@ property ( nonatomic , strong ) AiVM * aiVM ;
@ property ( nonatomic , strong ) AVAudioPlayer * aiAudioPlayer ;
@ property ( nonatomic , strong ) NSMutableData * voiceChatAudioBuffer ;
// 文 本 跟 踪
@ property ( nonatomic , strong ) NSMutableString * assistantVisibleText ;
@ property ( nonatomic , strong ) NSMutableString * deepgramFullText ;
// 日 志 节 流
@ property ( nonatomic , assign ) NSTimeInterval lastRMSLogTime ;
@ end
@ implementation KBAiMainVC
# pragma mark - Lifecycle
- ( void ) viewDidLoad {
[ super viewDidLoad ] ;
// 让 视 图 延 伸 到 屏 幕 边 缘 ( 包 括 状 态 栏 和 导 航 栏 下 方 )
self . edgesForExtendedLayout = UIRectEdgeAll ;
self . extendedLayoutIncludesOpaqueBars = YES ;
[ self setupUI ] ;
[ self setupOrchestrator ] ;
[ self setupStreamingManager ] ;
[ self setupDeepgramManager ] ;
}
- ( void ) viewWillAppear : ( BOOL ) animated {
[ super viewWillAppear : animated ] ;
// TabBar 背 景 色 由 BaseTabBarController 统 一 管 理 , 这 里 不 需 要 设 置
}
- ( void ) viewWillDisappear : ( BOOL ) animated {
[ super viewWillDisappear : animated ] ;
// 页 面 消 失 时 停 止 对 话
[ self . orchestrator stop ] ;
[ self . streamingManager disconnect ] ;
[ self . deepgramManager disconnect ] ;
}
- ( void ) viewDidLayoutSubviews {
[ super viewDidLayoutSubviews ] ;
// 只 更 新 mask 的 frame ( mask 已 在 setupUI 中 创 建 )
if ( self . blurEffectView . layer . mask ) {
self . blurEffectView . layer . mask . frame = self . blurEffectView . bounds ;
}
}
# pragma mark - UI Setup
- ( void ) setupUI {
self . view . backgroundColor = [ UIColor whiteColor ] ;
self . title = @ "AI 助手" ;
// 安 全 区 域
UILayoutGuide * safeArea = self . view . safeAreaLayoutGuide ;
// PersonImageView ( 背 景 图 , 最 底 层 )
self . personImageView =
[ [ UIImageView alloc ] initWithImage : [ UIImage imageNamed : @ "person_icon" ] ] ;
[ self . view addSubview : self . personImageView ] ;
[ self . personImageView mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . right . top . bottom . equalTo ( self . view ) ;
} ] ;
// TabBar 毛 玻 璃 模 糊 背 景 ( 在 personImageView 之 上 )
self . tabbarBackgroundView = [ [ UIView alloc ] init ] ;
self . tabbarBackgroundView . translatesAutoresizingMaskIntoConstraints = NO ;
self . tabbarBackgroundView . clipsToBounds = YES ;
[ self . view addSubview : self . tabbarBackgroundView ] ;
// 模 糊 效 果
UIBlurEffect * blurEffect =
[ UIBlurEffect effectWithStyle : UIBlurEffectStyleLight ] ;
self . blurEffectView = [ [ UIVisualEffectView alloc ] initWithEffect : blurEffect ] ;
self . blurEffectView . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . tabbarBackgroundView addSubview : self . blurEffectView ] ;
// 为 blurEffectView 创 建 透 明 度 渐 变
// mask ( 从 底 部 到 中 间 不 透 明 , 从 中 间 到 顶 部 透 明 )
CAGradientLayer * maskLayer = [ CAGradientLayer layer ] ;
maskLayer . startPoint = CGPointMake ( 0.5 , 1 ) ; // 底 部
maskLayer . endPoint = CGPointMake ( 0.5 , 0 ) ; // 顶 部
// 底 部 到 中 间 保 持 不 透 明 , 从 中 间 到 顶 部 过 渡 透 明
maskLayer . colors = @ [
( __bridge id ) [ UIColor whiteColor ] . CGColor , // 底 部 : 完 全 不 透 明
( __bridge id ) [ UIColor whiteColor ] . CGColor , // 中 间 : 完 全 不 透 明
( __bridge id ) [ UIColor clearColor ] . CGColor // 顶 部 : 完 全 透 明
] ;
maskLayer . locations = @ [ @ ( 0.0 ) , @ ( 0.5 ) , @ ( 1.0 ) ] ;
self . blurEffectView . layer . mask = maskLayer ;
// 状 态 标 签
self . statusLabel = [ [ UILabel alloc ] init ] ;
self . statusLabel . text = @ "按住按钮开始对话" ;
self . statusLabel . font = [ UIFont systemFontOfSize : 14 ] ;
self . statusLabel . textColor = [ UIColor secondaryLabelColor ] ;
self . statusLabel . textAlignment = NSTextAlignmentCenter ;
self . statusLabel . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . view addSubview : self . statusLabel ] ;
// 转 写 文 本 标 签
self . transcriptLabel = [ [ UILabel alloc ] init ] ;
self . transcriptLabel . text = @ "" ;
self . transcriptLabel . font = [ UIFont systemFontOfSize : 16 ] ;
self . transcriptLabel . textColor = [ UIColor labelColor ] ;
self . transcriptLabel . numberOfLines = 0 ;
self . transcriptLabel . textAlignment = NSTextAlignmentRight ;
self . transcriptLabel . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . view addSubview : self . transcriptLabel ] ;
// 聊 天 视 图
self . chatView = [ [ KBChatTableView alloc ] init ] ;
self . chatView . backgroundColor = [ UIColor clearColor ] ;
self . chatView . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . view addSubview : self . chatView ] ;
// 录 音 按 钮
self . recordButton = [ [ KBAiRecordButton alloc ] init ] ;
self . recordButton . delegate = self ;
self . recordButton . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . view addSubview : self . recordButton ] ;
// 评 论 按 钮 ( 聊 天 视 图 右 侧 居 中 )
self . commentButton = [ UIButton buttonWithType : UIButtonTypeCustom ] ;
[ self . commentButton setImage : [ UIImage systemImageNamed : @ "bubble.right.fill" ]
forState : UIControlStateNormal ] ;
self . commentButton . tintColor = [ UIColor whiteColor ] ;
self . commentButton . backgroundColor = [ UIColor systemBlueColor ] ;
self . commentButton . layer . cornerRadius = 25 ;
self . commentButton . layer . shadowColor = [ UIColor blackColor ] . CGColor ;
self . commentButton . layer . shadowOffset = CGSizeMake ( 0 , 2 ) ;
self . commentButton . layer . shadowOpacity = 0.3 ;
self . commentButton . layer . shadowRadius = 4 ;
self . commentButton . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . commentButton addTarget : self
action : @ selector ( showComment )
forControlEvents : UIControlEventTouchUpInside ] ;
[ self . view addSubview : self . commentButton ] ;
// 布 局 约 束 - 使 用 Masonry
[ self . tabbarBackgroundView mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . right . bottom . equalTo ( self . view ) ;
make . height . mas_equalTo ( KBFit ( 238 ) ) ;
} ] ;
[ self . blurEffectView mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . edges . equalTo ( self . tabbarBackgroundView ) ;
} ] ;
[ self . statusLabel mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . top . equalTo ( self . view . mas_safeAreaLayoutGuideTop ) . offset ( 8 ) ;
make . left . equalTo ( self . view ) . offset ( 16 ) ;
make . right . equalTo ( self . view ) . offset ( -16 ) ;
// 设 置 固 定 高 度 , 避 免 内 容 变 化 导 致 布 局 跳 动
make . height . mas_equalTo ( 20 ) ; // 单 行 文 本 高 度
} ] ;
[ self . transcriptLabel mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . top . equalTo ( self . statusLabel . mas_bottom ) . offset ( 8 ) ;
make . left . equalTo ( self . view ) . offset ( 16 ) ;
make . right . equalTo ( self . view ) . offset ( -16 ) ;
// 设 置 固 定 高 度 , 避 免 内 容 变 化 导 致 布 局 跳 动
make . height . mas_equalTo ( 60 ) ; // 根 据 实 际 需 要 调 整 高 度
} ] ;
// 设 置 内 容 压 缩 阻 力 , 避 免 被 压 缩
[ self . transcriptLabel setContentCompressionResistancePriority : UILayoutPriorityDefaultLow
forAxis : UILayoutConstraintAxisVertical ] ;
[ self . chatView mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . right . equalTo ( self . view ) ;
make . bottom . equalTo ( self . tabbarBackgroundView . mas_top ) . offset ( -8 ) ;
make . top . equalTo ( self . transcriptLabel . mas_bottom ) . offset ( 8 ) ;
// 设 置 最 小 高 度 , 避 免 被 压 缩 为 0
make . height . greaterThanOrEqualTo ( @ 100 ) . priority ( MASLayoutPriorityDefaultHigh ) ;
} ] ;
// chatView 应 该 尽 可 能 占 据 空 间
[ self . chatView setContentCompressionResistancePriority : UILayoutPriorityRequired
forAxis : UILayoutConstraintAxisVertical ] ;
[ self . recordButton mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . equalTo ( self . view . mas_safeAreaLayoutGuideLeft ) . offset ( 20 ) ;
make . right . equalTo ( self . view . mas_safeAreaLayoutGuideRight ) . offset ( -20 ) ;
make . bottom . equalTo ( self . view . mas_safeAreaLayoutGuideBottom ) . offset ( -16 ) ;
make . height . mas_equalTo ( 50 ) ;
} ] ;
[ self . commentButton mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . right . equalTo ( self . view . mas_safeAreaLayoutGuideRight ) . offset ( -16 ) ;
make . centerY . equalTo ( self . view ) ;
make . width . height . mas_equalTo ( 50 ) ;
} ] ;
}
# pragma mark - Orchestrator Setup
- ( void ) setupOrchestrator {
self . orchestrator = [ [ ConversationOrchestrator alloc ] init ] ;
// 配 置 服 务 器 地 址
// 1. ASR 语 音 识 别 服 务 ( WebSocket )
self . orchestrator . asrServerURL = @ "ws://192.168.2.21:7529/ws/asr" ;
// 2. LLM 大 语 言 模 型 服 务 ( HTTP Stream )
self . orchestrator . llmServerURL = @ "http://192.168.2.21:7529/api/chat/stream" ;
// 3. TTS 语 音 合 成 服 务 ( HTTP )
self . orchestrator . ttsServerURL = @ "http://192.168.2.21:7529/api/tts/stream" ;
__weak typeof ( self ) weakSelf = self ;
// 状 态 变 化 回 调
self . orchestrator . onStateChange = ^ ( ConversationState state ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf updateStatusForState : state ] ;
} ;
// 实 时 识 别 文 本 回 调
self . orchestrator . onPartialText = ^ ( NSString * text ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
strongSelf . statusLabel . text = text . length > 0 ? text : @ "正在识别..." ;
} ;
// 用 户 最 终 文 本 回 调
self . orchestrator . onUserFinalText = ^ ( NSString * text ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
if ( text . length > 0 ) {
[ strongSelf . chatView addUserMessage : text ] ;
}
} ;
// AI 可 见 文 本 回 调 ( 打 字 机 效 果 )
self . orchestrator . onAssistantVisibleText = ^ ( NSString * text ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf . chatView updateLastAssistantMessage : text ] ;
} ;
// AI 完 整 回 复 回 调
self . orchestrator . onAssistantFullText = ^ ( NSString * text ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf . chatView updateLastAssistantMessage : text ] ;
[ strongSelf . chatView markLastAssistantMessageComplete ] ;
} ;
// 音 量 更 新 回 调
self . orchestrator . onVolumeUpdate = ^ ( float rms ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf . recordButton updateVolumeRMS : rms ] ;
} ;
// AI 开 始 说 话
self . orchestrator . onSpeakingStart = ^ {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
// 添 加 空 的 AI 消 息 占 位
[ strongSelf . chatView addAssistantMessage : @ "" audioDuration : 0 audioData : nil ] ;
} ;
// AI 说 话 结 束
self . orchestrator . onSpeakingEnd = ^ {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf . chatView markLastAssistantMessageComplete ] ;
} ;
// 错 误 回 调
self . orchestrator . onError = ^ ( NSError * error ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf )
return ;
[ strongSelf showError : error ] ;
} ;
}
# pragma mark - Streaming Manager
- ( void ) setupStreamingManager {
self . streamingManager = [ [ VoiceChatStreamingManager alloc ] init ] ;
self . streamingManager . delegate = self ;
self . streamingManager . serverURL = @ "ws://192.168.2.21:7529/api/ws/chat" ;
self . assistantVisibleText = [ [ NSMutableString alloc ] init ] ;
self . voiceChatAudioBuffer = [ [ NSMutableData alloc ] init ] ;
self . lastRMSLogTime = 0 ;
}
# pragma mark - Deepgram Manager
- ( void ) setupDeepgramManager {
self . deepgramManager = [ [ DeepgramStreamingManager alloc ] init ] ;
self . deepgramManager . delegate = self ;
self . deepgramManager . serverURL = @ "wss://api.deepgram.com/v1/listen" ;
self . deepgramManager . apiKey = @ "9c792eb63a65d644cbc95785155754cd1e84f8cf" ;
self . deepgramManager . language = @ "en" ;
self . deepgramManager . model = @ "nova-3" ;
self . deepgramManager . punctuate = YES ;
self . deepgramManager . smartFormat = YES ;
self . deepgramManager . interimResults = YES ;
self . deepgramManager . encoding = @ "linear16" ;
self . deepgramManager . sampleRate = 16000.0 ;
self . deepgramManager . channels = 1 ;
[ self . deepgramManager prepareConnection ] ;
self . deepgramFullText = [ [ NSMutableString alloc ] init ] ;
self . aiVM = [ [ AiVM alloc ] init ] ;
}
# pragma mark - 事 件
- ( void ) showComment {
CGFloat customViewHeight = KB_SCREEN _HEIGHT * ( 0.8 ) ;
KBAICommentView * customView = [ [ KBAICommentView alloc ]
initWithFrame : CGRectMake ( 0 , 0 , KB_SCREEN _WIDTH , customViewHeight ) ] ;
LSTPopView * popView =
[ LSTPopView initWithCustomView : customView
parentView : nil
popStyle : LSTPopStyleSmoothFromBottom
dismissStyle : LSTDismissStyleSmoothToBottom ] ;
self . popView = popView ;
popView . priority = 1000 ;
popView . isAvoidKeyboard = false ;
popView . hemStyle = LSTHemStyleBottom ;
popView . dragStyle = LSTDragStyleY_Positive ;
popView . dragDistance = customViewHeight * 0.5 ;
popView . sweepStyle = LSTSweepStyleY_Positive ;
popView . swipeVelocity = 1600 ;
popView . sweepDismissStyle = LSTSweepDismissStyleSmooth ;
[ popView pop ] ;
}
- ( void ) showCommentDirectly {
if ( self . commentView . superview ) {
[ self . view bringSubviewToFront : self . commentView ] ;
return ;
}
CGFloat customViewHeight = KB_SCREEN _HEIGHT * ( 0.8 ) ;
KBAICommentView * customView =
[ [ KBAICommentView alloc ] initWithFrame : CGRectZero ] ;
customView . translatesAutoresizingMaskIntoConstraints = NO ;
[ self . view addSubview : customView ] ;
[ NSLayoutConstraint activateConstraints : @ [
[ customView . leadingAnchor constraintEqualToAnchor : self . view . leadingAnchor ] ,
[ customView . trailingAnchor
constraintEqualToAnchor : self . view . trailingAnchor ] ,
[ customView . bottomAnchor constraintEqualToAnchor : self . view . bottomAnchor ] ,
[ customView . heightAnchor constraintEqualToConstant : customViewHeight ] ,
] ] ;
self . commentView = customView ;
}
# pragma mark - 次 数 用 尽 弹 窗
- ( void ) showChatLimitPopWithMessage : ( NSString * ) message {
if ( self . limitPopView ) {
[ self . limitPopView dismiss ] ;
}
CGFloat width = 252.0 ;
CGFloat height = 252.0 + 18.0 + 53.0 + 18.0 + 28.0 ;
KBChatLimitPopView * content =
[ [ KBChatLimitPopView alloc ] initWithFrame : CGRectMake ( 0 , 0 , width , height ) ] ;
content . message = message ;
content . delegate = self ;
LSTPopView * popView =
[ LSTPopView initWithCustomView : content
parentView : nil
popStyle : LSTPopStyleFade
dismissStyle : LSTDismissStyleFade ] ;
popView . bgColor = [ [ UIColor blackColor ] colorWithAlphaComponent : 0.4 ] ;
popView . hemStyle = LSTHemStyleCenter ;
popView . isClickBgDismiss = YES ;
popView . isAvoidKeyboard = NO ;
self . limitPopView = popView ;
[ popView pop ] ;
}
# pragma mark - KBChatLimitPopViewDelegate
- ( void ) chatLimitPopViewDidTapCancel : ( KBChatLimitPopView * ) view {
[ self . limitPopView dismiss ] ;
}
- ( void ) chatLimitPopViewDidTapRecharge : ( KBChatLimitPopView * ) view {
[ self . limitPopView dismiss ] ;
if ( ! [ KBUserSessionManager shared ] . isLoggedIn ) {
[ [ KBUserSessionManager shared ] goLoginVC ] ;
return ;
}
KBPayMainVC * vc = [ [ KBPayMainVC alloc ] init ] ;
vc . initialSelectedIndex = 1 ; // SVIP
[ KB_CURRENT _NAV pushViewController : vc animated : true ] ;
}
# pragma mark - UI Updates
- ( void ) updateStatusForState : ( ConversationState ) state {
switch ( state ) {
case ConversationStateIdle :
self . statusLabel . text = @ "按住按钮开始对话" ;
self . recordButton . state = KBAiRecordButtonStateNormal ;
break ;
case ConversationStateListening :
self . statusLabel . text = @ "正在聆听..." ;
self . recordButton . state = KBAiRecordButtonStateRecording ;
break ;
case ConversationStateRecognizing :
self . statusLabel . text = @ "正在识别..." ;
self . recordButton . state = KBAiRecordButtonStateNormal ;
break ;
case ConversationStateThinking :
self . statusLabel . text = @ "AI 正在思考..." ;
self . recordButton . state = KBAiRecordButtonStateNormal ;
break ;
case ConversationStateSpeaking :
self . statusLabel . text = @ "AI 正在回复..." ;
self . recordButton . state = KBAiRecordButtonStateNormal ;
break ;
}
}
- ( void ) showError : ( NSError * ) error {
UIAlertController * alert =
[ UIAlertController alertControllerWithTitle : @ "错误"
message : error . localizedDescription
preferredStyle : UIAlertControllerStyleAlert ] ;
[ alert addAction : [ UIAlertAction actionWithTitle : @ "确定"
style : UIAlertActionStyleDefault
handler : nil ] ] ;
[ self presentViewController : alert animated : YES completion : nil ] ;
}
# pragma mark - KBAiRecordButtonDelegate
- ( void ) recordButtonDidBeginPress : ( KBAiRecordButton * ) button {
NSLog ( @ "[KBAiMainVC] Record button began press" ) ;
// 停 止 正 在 播 放 的 音 频
[ self . chatView stopPlayingAudio ] ;
NSString * token = [ [ KBUserSessionManager shared ] accessToken ] ? : @ "" ;
if ( token . length = = 0 ) {
[ [ KBUserSessionManager shared ] goLoginVC ] ;
return ;
}
self . statusLabel . text = @ "正在连接..." ;
self . recordButton . state = KBAiRecordButtonStateRecording ;
[ self . deepgramFullText setString : @ "" ] ;
self . transcriptLabel . text = @ "" ;
[ self . deepgramManager start ] ;
}
- ( void ) recordButtonDidEndPress : ( KBAiRecordButton * ) button {
NSLog ( @ "[KBAiMainVC] Record button end press" ) ;
[ self . deepgramManager stopAndFinalize ] ;
}
- ( void ) recordButtonDidCancelPress : ( KBAiRecordButton * ) button {
NSLog ( @ "[KBAiMainVC] Record button cancel press" ) ;
[ self . deepgramManager cancel ] ;
}
# pragma mark - VoiceChatStreamingManagerDelegate
- ( void ) voiceChatStreamingManagerDidConnect {
self . statusLabel . text = @ "已连接,准备中..." ;
}
- ( void ) voiceChatStreamingManagerDidDisconnect : ( NSError * _Nullable ) error {
self . recordButton . state = KBAiRecordButtonStateNormal ;
if ( error ) {
[ self showError : error ] ;
}
}
- ( void ) voiceChatStreamingManagerDidStartSession : ( NSString * ) sessionId {
self . statusLabel . text = @ "正在聆听..." ;
self . recordButton . state = KBAiRecordButtonStateRecording ;
}
- ( void ) voiceChatStreamingManagerDidStartTurn : ( NSInteger ) turnIndex {
self . statusLabel . text = @ "正在聆听..." ;
self . recordButton . state = KBAiRecordButtonStateRecording ;
}
- ( void ) voiceChatStreamingManagerDidReceiveEagerEndOfTurnWithTranscript : ( NSString * ) text
confidence : ( double ) confidence {
self . statusLabel . text = @ "准备响应..." ;
}
- ( void ) voiceChatStreamingManagerDidResumeTurn {
self . statusLabel . text = @ "正在聆听..." ;
}
- ( void ) voiceChatStreamingManagerDidUpdateRMS : ( float ) rms {
[ self . recordButton updateVolumeRMS : rms ] ;
NSTimeInterval now = [ [ NSDate date ] timeIntervalSince1970 ] ;
if ( now - self . lastRMSLogTime >= 1.0 ) {
self . lastRMSLogTime = now ;
NSLog ( @ "[KBAiMainVC] RMS: %.3f" , rms ) ;
}
}
- ( void ) voiceChatStreamingManagerDidReceiveInterimTranscript : ( NSString * ) text {
self . statusLabel . text = @ "正在识别..." ;
if ( text . length > 0 ) {
self . transcriptLabel . text = text ;
}
}
- ( void ) voiceChatStreamingManagerDidReceiveFinalTranscript : ( NSString * ) text {
if ( text . length > 0 ) {
self . transcriptLabel . text = @ "" ;
[ self . chatView addUserMessage : text ] ;
}
}
- ( void ) voiceChatStreamingManagerDidReceiveLLMStart {
self . statusLabel . text = @ "AI 正在思考..." ;
[ self . assistantVisibleText setString : @ "" ] ;
[ self . chatView addAssistantMessage : @ "" audioDuration : 0 audioData : nil ] ;
[ self . voiceChatAudioBuffer setLength : 0 ] ;
}
- ( void ) voiceChatStreamingManagerDidReceiveLLMToken : ( NSString * ) token {
if ( token . length = = 0 ) {
return ;
}
[ self . assistantVisibleText appendString : token ] ;
[ self . chatView updateLastAssistantMessage : self . assistantVisibleText ] ;
}
- ( void ) voiceChatStreamingManagerDidReceiveAudioChunk : ( NSData * ) audioData {
if ( audioData . length = = 0 ) {
return ;
}
[ self . voiceChatAudioBuffer appendData : audioData ] ;
}
- ( void ) voiceChatStreamingManagerDidCompleteWithTranscript : ( NSString * ) transcript
aiResponse : ( NSString * ) aiResponse {
NSString * finalText = aiResponse . length > 0 ? aiResponse : self . assistantVisibleText ;
if ( aiResponse . length > 0 ) {
[ self . assistantVisibleText setString : aiResponse ] ;
}
// 计 算 音 频 时 长
NSTimeInterval duration = 0 ;
if ( self . voiceChatAudioBuffer . length > 0 ) {
NSError * error = nil ;
AVAudioPlayer * player = [ [ AVAudioPlayer alloc ] initWithData : self . voiceChatAudioBuffer
error : & error ] ;
if ( ! error && player ) {
duration = player . duration ;
}
}
if ( finalText . length > 0 ) {
[ self . chatView updateLastAssistantMessage : finalText ] ;
[ self . chatView markLastAssistantMessageComplete ] ;
} else if ( transcript . length > 0 ) {
[ self . chatView addAssistantMessage : transcript
audioDuration : duration
audioData : self . voiceChatAudioBuffer . length > 0 ? self . voiceChatAudioBuffer : nil ] ;
}
if ( self . voiceChatAudioBuffer . length > 0 ) {
[ self playAiAudioData : self . voiceChatAudioBuffer ] ;
[ self . voiceChatAudioBuffer setLength : 0 ] ;
}
self . recordButton . state = KBAiRecordButtonStateNormal ;
self . statusLabel . text = @ "完成" ;
}
- ( void ) voiceChatStreamingManagerDidFail : ( NSError * ) error {
self . recordButton . state = KBAiRecordButtonStateNormal ;
[ self showError : error ] ;
}
# pragma mark - DeepgramStreamingManagerDelegate
- ( void ) deepgramStreamingManagerDidConnect {
self . statusLabel . text = @ "已连接,准备中..." ;
}
- ( void ) deepgramStreamingManagerDidDisconnect : ( NSError * _Nullable ) error {
self . recordButton . state = KBAiRecordButtonStateNormal ;
if ( error ) {
[ self showError : error ] ;
}
}
- ( void ) deepgramStreamingManagerDidUpdateRMS : ( float ) rms {
[ self . recordButton updateVolumeRMS : rms ] ;
NSTimeInterval now = [ [ NSDate date ] timeIntervalSince1970 ] ;
if ( now - self . lastRMSLogTime >= 1.0 ) {
self . lastRMSLogTime = now ;
NSLog ( @ "[KBAiMainVC] RMS: %.3f" , rms ) ;
}
}
- ( void ) deepgramStreamingManagerDidReceiveInterimTranscript : ( NSString * ) text {
self . statusLabel . text = @ "正在识别..." ;
NSString * displayText = text ? : @ "" ;
if ( self . deepgramFullText . length > 0 && displayText . length > 0 ) {
displayText =
[ NSString stringWithFormat : @ "%@ %@" , self . deepgramFullText , displayText ] ;
} else if ( self . deepgramFullText . length > 0 ) {
displayText = [ self . deepgramFullText copy ] ;
}
self . transcriptLabel . text = displayText ;
}
- ( void ) deepgramStreamingManagerDidReceiveFinalTranscript : ( NSString * ) text {
if ( text . length > 0 ) {
if ( self . deepgramFullText . length > 0 ) {
[ self . deepgramFullText appendString : @ " " ] ;
}
[ self . deepgramFullText appendString : text ] ;
}
self . transcriptLabel . text = self . deepgramFullText ;
self . statusLabel . text = @ "识别完成" ;
self . recordButton . state = KBAiRecordButtonStateNormal ;
NSString * finalText = [ self . deepgramFullText copy ] ;
if ( finalText . length = = 0 ) {
return ;
}
// 添 加 用 户 消 息
[ self . chatView addUserMessage : finalText ] ;
__weak typeof ( self ) weakSelf = self ;
[ KBHUD showWithStatus : @ "AI 思考中..." ] ;
// 请 求 chat / message 接 口
[ self . aiVM requestChatMessageWithContent : finalText
companionId : 0
completion : ^ ( KBAiMessageResponse * _Nullable response ,
NSError * _Nullable error ) {
__strong typeof ( weakSelf ) strongSelf = weakSelf ;
if ( ! strongSelf ) {
return ;
}
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ KBHUD dismiss ] ;
if ( error ) {
[ KBHUD showError : error . localizedDescription ? : @ "请求失败" ] ;
return ;
}
if ( response . code = = 50030 ) {
NSString * message = response . message ? : @ "" ;
[ strongSelf showChatLimitPopWithMessage : message ] ;
return ;
}
if ( ! response || ! response . data ) {
NSString * message = response . message ? : @ "AI 回复为空" ;
[ KBHUD showError : message ] ;
return ;
}
// 获 取 AI 回 复 文 本
NSString * aiResponse = response . data . aiResponse ? : response . data . content ? : response . data . text ? : response . data . message ? : @ "" ;
if ( aiResponse . length = = 0 ) {
[ KBHUD showError : @ "AI 回复为空" ] ;
return ;
}
// 获 取 audioId
NSString * audioId = response . data . audioId ;
// 添 加 AI 消 息 ( 带 audioId )
[ strongSelf . chatView addAssistantMessage : aiResponse
audioId : audioId ] ;
} ) ;
} ] ;
}
- ( void ) deepgramStreamingManagerDidFail : ( NSError * ) error {
self . recordButton . state = KBAiRecordButtonStateNormal ;
[ self showError : error ] ;
}
# pragma mark - Audio Playback
- ( void ) playAiAudioData : ( NSData * ) audioData {
if ( audioData . length = = 0 ) {
return ;
}
NSError * sessionError = nil ;
AudioSessionManager * audioSession = [ AudioSessionManager sharedManager ] ;
if ( ! [ audioSession configureForPlayback : & sessionError ] ) {
NSLog ( @ "[KBAiMainVC] Configure playback failed: %@" ,
sessionError . localizedDescription ? : @ "" ) ;
}
if ( ! [ audioSession activateSession : & sessionError ] ) {
NSLog ( @ "[KBAiMainVC] Activate playback session failed: %@" ,
sessionError . localizedDescription ? : @ "" ) ;
}
NSError * error = nil ;
self . aiAudioPlayer = [ [ AVAudioPlayer alloc ] initWithData : audioData
error : & error ] ;
if ( error || ! self . aiAudioPlayer ) {
NSLog ( @ "[KBAiMainVC] Audio player init failed: %@" ,
error . localizedDescription ? : @ "" ) ;
return ;
}
self . aiAudioPlayer . delegate = self ;
[ self . aiAudioPlayer prepareToPlay ] ;
[ self . aiAudioPlayer play ] ;
}
# pragma mark - AVAudioPlayerDelegate
- ( void ) audioPlayerDidFinishPlaying : ( AVAudioPlayer * ) player
successfully : ( BOOL ) flag {
[ [ AudioSessionManager sharedManager ] deactivateSession ] ;
}
@ end