处理语音

This commit is contained in:
2026-01-29 20:56:24 +08:00
parent 23c0d14128
commit 36135313d8
8 changed files with 236 additions and 9 deletions

View File

@@ -16,6 +16,7 @@
@property (nonatomic, strong) UIView *bubbleView;
@property (nonatomic, strong, readwrite) UILabel *messageLabel; // readwrite
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
@property (nonatomic, strong) UIActivityIndicatorView *messageLoadingIndicator; // AI
@property (nonatomic, strong) KBAiChatMessage *currentMessage;
//
@@ -61,6 +62,12 @@
self.loadingIndicator.hidesWhenStopped = YES;
[self.contentView addSubview:self.loadingIndicator];
// AI AI
self.messageLoadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
self.messageLoadingIndicator.color = [UIColor whiteColor];
self.messageLoadingIndicator.hidesWhenStopped = YES;
[self.contentView addSubview:self.messageLoadingIndicator];
//
self.bubbleView = [[UIView alloc] init];
self.bubbleView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.7];
@@ -83,8 +90,7 @@
[self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.top.equalTo(self.contentView).offset(8);
// make.width.height.mas_equalTo(24);
make.width.height.mas_equalTo(0);
make.width.height.mas_equalTo(24);
}];
[self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -96,6 +102,12 @@
make.center.equalTo(self.voiceButton);
}];
// AI bubbleView
[self.messageLoadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.top.equalTo(self.voiceButton.mas_bottom).offset(12);
}];
// bubbleView
[self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.voiceButton.mas_bottom).offset(4);
@@ -116,8 +128,8 @@
}
- (void)configureWithMessage:(KBAiChatMessage *)message {
NSLog(@"[KBChatAssistantMessageCell] 配置消息 - 文本长度: %lu, isComplete: %d, needsTypewriter: %d, 打字机运行中: %d",
(unsigned long)message.text.length, message.isComplete, message.needsTypewriterEffect,
NSLog(@"[KBChatAssistantMessageCell] 配置消息 - 文本长度: %lu, isComplete: %d, needsTypewriter: %d, isLoading: %d, 打字机运行中: %d",
(unsigned long)message.text.length, message.isComplete, message.needsTypewriterEffect, message.isLoading,
(self.typewriterTimer && self.typewriterTimer.isValid));
//
@@ -125,6 +137,21 @@
self.currentMessage = message;
// loading
if (message.isLoading) {
self.messageLabel.attributedText = nil;
self.messageLabel.text = @"";
self.bubbleView.hidden = YES;
self.voiceButton.hidden = YES;
self.durationLabel.hidden = YES;
[self.messageLoadingIndicator startAnimating];
return;
}
// loading loading
[self.messageLoadingIndicator stopAnimating];
self.bubbleView.hidden = NO;
// 使
if (message.needsTypewriterEffect && !message.isComplete && message.text.length > 0) {
//

View File

@@ -54,6 +54,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 清空所有消息
- (void)clearMessages;
/// 移除 loading AI 消息
- (void)removeLoadingAssistantMessage;
/// 滚动到底部
- (void)scrollToBottom;

View File

@@ -236,6 +236,19 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
[self updateFooterVisibility];
}
- (void)removeLoadingAssistantMessage {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) {
[self.messages removeObjectAtIndex:i];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
NSLog(@"[KBChatTableView] 移除 loading AI 消息,索引: %ld", (long)i);
break;
}
}
}
- (void)scrollToBottom {
[self scrollToBottomAnimated:YES];
}
@@ -339,6 +352,11 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
});
});
}
//
if (message.type == KBAiChatMessageTypeAssistant && message.audioId.length > 0) {
[self preloadAudioForMessage:message];
}
}
- (void)reloadWithMessages:(NSArray<KBAiChatMessage *> *)messages
@@ -620,8 +638,8 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
[cell showLoadingAnimation];
}
// 50.5
[self pollAudioForMessage:message atIndexPath:indexPath retryCount:0 maxRetries:5];
// 10110
[self pollAudioForMessage:message atIndexPath:indexPath retryCount:0 maxRetries:10];
}
- (void)pollAudioForMessage:(KBAiChatMessage *)message
@@ -638,6 +656,7 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
dispatch_async(dispatch_get_main_queue(), ^{
// audioURL
if (!error && audioURL.length > 0) {
NSLog(@"[KBChatTableView] 音频 URL 获取成功(第 %ld 次)", (long)(retryCount + 1));
//
[strongSelf downloadAndPlayAudioFromURL:audioURL
forMessage:message
@@ -647,11 +666,11 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
//
if (retryCount < maxRetries - 1) {
NSLog(@"[KBChatTableView] 音频未就绪,0.5秒后重试 (%ld/%ld)",
NSLog(@"[KBChatTableView] 音频未就绪,1秒后重试 (%ld/%ld)",
(long)(retryCount + 1), (long)maxRetries);
// 0.5
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
// 1
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[strongSelf pollAudioForMessage:message
atIndexPath:indexPath
@@ -786,6 +805,115 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
}
}
#pragma mark - Audio Preload ()
///
- (void)preloadAudioForMessage:(KBAiChatMessage *)message {
if (!message || message.audioId.length == 0) {
return;
}
//
if (message.audioData && message.audioData.length > 0) {
NSLog(@"[KBChatTableView] 音频已缓存,跳过预加载");
return;
}
NSLog(@"[KBChatTableView] 开始预加载音频audioId: %@", message.audioId);
//
NSDate *startTime = [NSDate date];
// 10110
[self pollPreloadAudioForMessage:message retryCount:0 maxRetries:10 startTime:startTime];
}
///
- (void)pollPreloadAudioForMessage:(KBAiChatMessage *)message
retryCount:(NSInteger)retryCount
maxRetries:(NSInteger)maxRetries
startTime:(NSDate *)startTime {
__weak typeof(self) weakSelf = self;
[self.aiVM requestAudioWithAudioId:message.audioId
completion:^(NSString *_Nullable audioURL, NSError *_Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
dispatch_async(dispatch_get_main_queue(), ^{
// audioURL
if (!error && audioURL.length > 0) {
// audioURL
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[KBChatTableView] ✅ 预加载音频 URL 获取成功(第 %ld 次),耗时: %.2f 秒", (long)(retryCount + 1), elapsed);
//
[strongSelf downloadAudioFromURL:audioURL forMessage:message startTime:startTime];
return;
}
//
if (retryCount < maxRetries - 1) {
NSLog(@"[KBChatTableView] 预加载音频未就绪1秒后重试 (%ld/%ld)",
(long)(retryCount + 1), (long)maxRetries);
// 1
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[strongSelf pollPreloadAudioForMessage:message
retryCount:retryCount + 1
maxRetries:maxRetries
startTime:startTime];
});
} else {
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[KBChatTableView] ❌ 预加载音频失败,已重试 %ld 次,总耗时: %.2f 秒", (long)maxRetries, elapsed);
}
});
}];
}
///
- (void)downloadAudioFromURL:(NSString *)urlString
forMessage:(KBAiChatMessage *)message
startTime:(NSDate *)startTime {
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSLog(@"[KBChatTableView] 预加载:无效的音频 URL: %@", urlString);
return;
}
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:^(NSData *_Nullable data,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error || !data || data.length == 0) {
NSLog(@"[KBChatTableView] 预加载:下载音频失败: %@", error.localizedDescription ?: @"");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
//
message.audioData = data;
//
NSError *playerError = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:&playerError];
if (!playerError && player) {
message.audioDuration = player.duration;
}
//
NSTimeInterval totalElapsed = [[NSDate date] timeIntervalSinceDate:startTime];
NSLog(@"[KBChatTableView] ✅ 预加载音频完成,音频时长: %.2f秒,总耗时: %.2f 秒", message.audioDuration, totalElapsed);
});
}];
[task resume];
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {

View File

@@ -37,6 +37,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)appendAssistantMessage:(NSString *)text
audioId:(nullable NSString *)audioId;
/// 添加 loading AI 消息
- (void)appendLoadingAssistantMessage;
/// 移除 loading AI 消息
- (void)removeLoadingAssistantMessage;
/// 更新聊天列表底部 inset
- (void)updateChatViewBottomInset:(CGFloat)bottomInset;

View File

@@ -503,6 +503,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
}
[self ensureOpeningMessageAtTop];
// loading
[self removeLoadingAssistantMessage];
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
audioId:audioId];
message.needsTypewriterEffect = YES;
@@ -510,6 +514,33 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
[self.chatView addMessage:message autoScroll:YES];
}
/// loading AI
- (void)appendLoadingAssistantMessage {
if (!self.messages) {
self.messages = [NSMutableArray array];
}
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage loadingAssistantMessage];
[self.messages addObject:message];
[self.chatView addMessage:message autoScroll:YES];
}
/// loading AI
- (void)removeLoadingAssistantMessage {
//
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeAssistant && message.isLoading) {
[self.messages removeObjectAtIndex:i];
break;
}
}
// chatView
[self.chatView removeLoadingAssistantMessage];
}
- (void)updateChatViewBottomInset:(CGFloat)bottomInset {
[self.chatView updateContentBottomInset:bottomInset];
}