This commit is contained in:
2026-01-27 13:57:32 +08:00
parent e8b4b2c58a
commit ce889e1ed0
9 changed files with 497 additions and 6 deletions

View File

@@ -72,6 +72,7 @@
#define API_AI_VOICE_TALK @"/chat/voice" // 语音对话(替换为后端真实路径) #define API_AI_VOICE_TALK @"/chat/voice" // 语音对话(替换为后端真实路径)
#define API_AI_CHAT_SYNC @"/chat/sync" // 同步对话 #define API_AI_CHAT_SYNC @"/chat/sync" // 同步对话
#define API_AI_CHAT_MESSAGE @"/chat/message" // 文本润色 #define API_AI_CHAT_MESSAGE @"/chat/message" // 文本润色
#define API_AI_AUDIO_UPLOAD @"/chat/audio/upload" // 语音上传(替换为后端真实路径)

View File

@@ -209,6 +209,7 @@
04E038E82F20E877002CA5A0 /* DeepgramWebSocketClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */; }; 04E038E82F20E877002CA5A0 /* DeepgramWebSocketClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */; };
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */; }; 04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */; };
04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */; }; 04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */; };
04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */; };
04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; }; 04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E038EE2F21F0EC002CA5A0 /* AiVM.m */; };
04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */; }; 04E0394B2F236E75002CA5A0 /* KBChatUserMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */; };
04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039482F236E75002CA5A0 /* KBChatTimeCell.m */; }; 04E0394C2F236E75002CA5A0 /* KBChatTimeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04E039482F236E75002CA5A0 /* KBChatTimeCell.m */; };
@@ -653,6 +654,8 @@
04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramStreamingManager.m; sourceTree = "<group>"; }; 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramStreamingManager.m; sourceTree = "<group>"; };
04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = "<group>"; }; 04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceToTextManager.h; sourceTree = "<group>"; };
04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceToTextManager.m; sourceTree = "<group>"; }; 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceToTextManager.m; sourceTree = "<group>"; };
04E0B2002F300002002CA5A0 /* KBVoiceRecordManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVoiceRecordManager.h; sourceTree = "<group>"; };
04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBVoiceRecordManager.m; sourceTree = "<group>"; };
04E038E62F20E877002CA5A0 /* DeepgramWebSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeepgramWebSocketClient.h; sourceTree = "<group>"; }; 04E038E62F20E877002CA5A0 /* DeepgramWebSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeepgramWebSocketClient.h; sourceTree = "<group>"; };
04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramWebSocketClient.m; sourceTree = "<group>"; }; 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DeepgramWebSocketClient.m; sourceTree = "<group>"; };
04E038ED2F21F0EC002CA5A0 /* AiVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AiVM.h; sourceTree = "<group>"; }; 04E038ED2F21F0EC002CA5A0 /* AiVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AiVM.h; sourceTree = "<group>"; };
@@ -1079,6 +1082,8 @@
04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */, 04E038E52F20E877002CA5A0 /* DeepgramStreamingManager.m */,
04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */, 04E0B1002F300001002CA5A0 /* KBVoiceToTextManager.h */,
04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */, 04E0B1012F300001002CA5A0 /* KBVoiceToTextManager.m */,
04E0B2002F300002002CA5A0 /* KBVoiceRecordManager.h */,
04E0B2012F300002002CA5A0 /* KBVoiceRecordManager.m */,
04E038E62F20E877002CA5A0 /* DeepgramWebSocketClient.h */, 04E038E62F20E877002CA5A0 /* DeepgramWebSocketClient.h */,
04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */, 04E038E72F20E877002CA5A0 /* DeepgramWebSocketClient.m */,
04E038ED2F21F0EC002CA5A0 /* AiVM.h */, 04E038ED2F21F0EC002CA5A0 /* AiVM.h */,
@@ -2331,6 +2336,7 @@
04E038E82F20E877002CA5A0 /* DeepgramWebSocketClient.m in Sources */, 04E038E82F20E877002CA5A0 /* DeepgramWebSocketClient.m in Sources */,
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */, 04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */,
04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */, 04E0B1022F300001002CA5A0 /* KBVoiceToTextManager.m in Sources */,
04E0B2022F300002002CA5A0 /* KBVoiceRecordManager.m in Sources */,
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */, 048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */,
0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */, 0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */,
048FFD142F274342005D62AE /* KBPersonaChatCell.m in Sources */, 048FFD142F274342005D62AE /* KBPersonaChatCell.m in Sources */,

View File

@@ -9,11 +9,13 @@
#import "KBPersonaChatCell.h" #import "KBPersonaChatCell.h"
#import "KBPersonaModel.h" #import "KBPersonaModel.h"
#import "KBVoiceInputBar.h" #import "KBVoiceInputBar.h"
#import "KBVoiceRecordManager.h"
#import "KBVoiceToTextManager.h" #import "KBVoiceToTextManager.h"
#import "AiVM.h" #import "AiVM.h"
#import "KBHUD.h"
#import <Masonry/Masonry.h> #import <Masonry/Masonry.h>
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate> @interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate>
/// ///
@property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UICollectionView *collectionView;
@@ -24,6 +26,9 @@
/// ///
@property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager; @property (nonatomic, strong) KBVoiceToTextManager *voiceToTextManager;
///
@property (nonatomic, strong) KBVoiceRecordManager *voiceRecordManager;
/// ///
@property (nonatomic, strong) NSMutableArray<KBPersonaModel *> *personas; @property (nonatomic, strong) NSMutableArray<KBPersonaModel *> *personas;
@@ -66,6 +71,7 @@
[self setupUI]; [self setupUI];
[self setupVoiceToTextManager]; [self setupVoiceToTextManager];
[self setupVoiceRecordManager];
[self loadPersonas]; [self loadPersonas];
} }
@@ -245,9 +251,17 @@
- (void)setupVoiceToTextManager { - (void)setupVoiceToTextManager {
self.voiceToTextManager = [[KBVoiceToTextManager alloc] initWithInputBar:self.voiceInputBar]; self.voiceToTextManager = [[KBVoiceToTextManager alloc] initWithInputBar:self.voiceInputBar];
self.voiceToTextManager.delegate = self; self.voiceToTextManager.delegate = self;
self.voiceToTextManager.deepgramEnabled = NO;
[self.voiceToTextManager prepareConnection]; [self.voiceToTextManager prepareConnection];
} }
/// 5
- (void)setupVoiceRecordManager {
self.voiceRecordManager = [[KBVoiceRecordManager alloc] init];
self.voiceRecordManager.delegate = self;
self.voiceRecordManager.minRecordDuration = 1.0;
}
- (NSInteger)currentCompanionId { - (NSInteger)currentCompanionId {
if (self.personas.count == 0) { if (self.personas.count == 0) {
return 0; return 0;
@@ -381,4 +395,38 @@
NSLog(@"[KBAIHomeVC] 语音识别失败:%@", error.localizedDescription); NSLog(@"[KBAIHomeVC] 语音识别失败:%@", error.localizedDescription);
} }
- (void)voiceToTextManagerDidBeginRecording:(KBVoiceToTextManager *)manager {
[self.voiceRecordManager startRecording];
}
- (void)voiceToTextManagerDidEndRecording:(KBVoiceToTextManager *)manager {
[self.voiceRecordManager stopRecording];
}
- (void)voiceToTextManagerDidCancelRecording:(KBVoiceToTextManager *)manager {
[self.voiceRecordManager cancelRecording];
}
#pragma mark - KBVoiceRecordManagerDelegate
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
didFinishRecordingAtURL:(NSURL *)fileURL
duration:(NSTimeInterval)duration {
NSDictionary *attributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:fileURL.path
error:nil];
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
}
- (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager {
NSLog(@"[KBAIHomeVC] 录音过短,已忽略");
[KBHUD showError:KBLocalized(@"录音时间过短,请重新录音")];
}
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
didFailWithError:(NSError *)error {
NSLog(@"[KBAIHomeVC] 录音失败:%@", error.localizedDescription);
}
@end @end

View File

@@ -44,6 +44,8 @@ typedef void (^AiVMMessageCompletion)(KBAiMessageResponse *_Nullable response,
typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL, typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL,
NSError *_Nullable error); NSError *_Nullable error);
typedef void (^AiVMUploadAudioCompletion)(NSString *_Nullable fileURL,
NSError *_Nullable error);
@interface AiVM : NSObject @interface AiVM : NSObject
@@ -58,6 +60,10 @@ typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL,
- (void)requestAudioWithAudioId:(NSString *)audioId - (void)requestAudioWithAudioId:(NSString *)audioId
completion:(AiVMAudioURLCompletion)completion; completion:(AiVMAudioURLCompletion)completion;
/// 上传语音文件m4a
- (void)uploadAudioFileAtURL:(NSURL *)fileURL
completion:(AiVMUploadAudioCompletion)completion;
#pragma mark - 人设相关接口 #pragma mark - 人设相关接口
/// 分页查询人设列表 /// 分页查询人设列表

View File

@@ -216,6 +216,51 @@ autoShowBusinessError:NO
}]; }];
} }
- (void)uploadAudioFileAtURL:(NSURL *)fileURL
completion:(AiVMUploadAudioCompletion)completion {
if (!fileURL || !fileURL.isFileURL) {
NSError *error = [NSError errorWithDomain:@"AiVM"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"invalid fileURL"}];
if (completion) {
completion(nil, error);
}
return;
}
[[KBNetworkManager shared] uploadFile:API_AI_AUDIO_UPLOAD
fileURL:fileURL
name:@"file"
mimeType:@"audio/mp4"
parameters:nil
headers:nil
completion:^(NSDictionary *_Nullable json,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error) {
if (completion) {
completion(nil, error);
}
return;
}
NSString *fileURLString = nil;
id dataObj = json[@"data"];
if ([dataObj isKindOfClass:[NSString class]]) {
fileURLString = (NSString *)dataObj;
} else if ([dataObj isKindOfClass:[NSDictionary class]]) {
id urlObj = dataObj[@"url"] ?: dataObj[@"audioUrl"];
if ([urlObj isKindOfClass:[NSString class]]) {
fileURLString = (NSString *)urlObj;
}
}
if (completion) {
completion(fileURLString, nil);
}
}];
}
#pragma mark - #pragma mark -
- (void)fetchPersonasWithPageNum:(NSInteger)pageNum - (void)fetchPersonasWithPageNum:(NSInteger)pageNum

View File

@@ -0,0 +1,47 @@
//
// KBVoiceRecordManager.h
// keyBoard
//
// Created by Mac on 2026/1/26.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KBVoiceRecordManagerErrorCode) {
KBVoiceRecordManagerErrorPermissionDenied = 1,
KBVoiceRecordManagerErrorSessionFailed = 2,
KBVoiceRecordManagerErrorRecorderFailed = 3,
KBVoiceRecordManagerErrorTooShort = 4,
KBVoiceRecordManagerErrorInterrupted = 5,
};
@class KBVoiceRecordManager;
@protocol KBVoiceRecordManagerDelegate <NSObject>
@optional
- (void)voiceRecordManagerDidStartRecording:(KBVoiceRecordManager *)manager;
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
didFinishRecordingAtURL:(NSURL *)fileURL
duration:(NSTimeInterval)duration;
- (void)voiceRecordManagerDidCancelRecording:(KBVoiceRecordManager *)manager;
- (void)voiceRecordManagerDidRecordTooShort:(KBVoiceRecordManager *)manager;
- (void)voiceRecordManager:(KBVoiceRecordManager *)manager
didFailWithError:(NSError *)error;
@end
/// 录音管理器AAC/M4A16kmono
@interface KBVoiceRecordManager : NSObject
@property(nonatomic, weak) id<KBVoiceRecordManagerDelegate> delegate;
@property(nonatomic, assign) NSTimeInterval minRecordDuration;
@property(nonatomic, assign, readonly, getter=isRecording) BOOL recording;
- (void)startRecording;
- (void)stopRecording;
- (void)cancelRecording;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,289 @@
//
// KBVoiceRecordManager.m
// keyBoard
//
// Created by Mac on 2026/1/26.
//
#import "KBVoiceRecordManager.h"
#import <AVFoundation/AVFoundation.h>
static NSString *const kKBVoiceRecordManagerErrorDomain =
@"KBVoiceRecordManager";
@interface KBVoiceRecordManager () <AVAudioRecorderDelegate>
@property(nonatomic, strong) AVAudioRecorder *recorder;
@property(nonatomic, strong) NSURL *currentFileURL;
@property(nonatomic, assign) BOOL recording;
@property(nonatomic, assign) BOOL stopping;
@property(nonatomic, assign) BOOL cancelled;
@property(nonatomic, assign) BOOL interrupted;
@property(nonatomic, assign) NSTimeInterval recordStartTime;
@end
@implementation KBVoiceRecordManager
- (instancetype)init {
self = [super init];
if (self) {
_minRecordDuration = 1.0;
[self setupNotifications];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Public Methods
- (void)startRecording {
if (self.recording) {
return;
}
AVAudioSession *session = [AVAudioSession sharedInstance];
if (session.recordPermission == AVAudioSessionRecordPermissionUndetermined) {
__weak typeof(self) weakSelf = self;
[session requestRecordPermission:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
if (granted) {
[weakSelf startRecordingInternal];
} else {
[weakSelf reportErrorWithCode:KBVoiceRecordManagerErrorPermissionDenied
message:@"Microphone permission denied"];
}
});
}];
return;
}
if (session.recordPermission != AVAudioSessionRecordPermissionGranted) {
[self reportErrorWithCode:KBVoiceRecordManagerErrorPermissionDenied
message:@"Microphone permission denied"];
return;
}
[self startRecordingInternal];
}
- (void)stopRecording {
if (!self.recording || !self.recorder) {
return;
}
self.stopping = YES;
self.cancelled = NO;
self.interrupted = NO;
[self.recorder stop];
}
- (void)cancelRecording {
if (!self.recording || !self.recorder) {
return;
}
self.cancelled = YES;
self.stopping = NO;
self.interrupted = NO;
[self.recorder stop];
}
#pragma mark - Private Methods
- (void)startRecordingInternal {
NSError *sessionError = nil;
if (![self configureSession:&sessionError]) {
[self reportErrorWithCode:KBVoiceRecordManagerErrorSessionFailed
message:sessionError.localizedDescription ?: @"Session error"];
return;
}
NSURL *fileURL = [self generateFileURL];
if (!fileURL) {
[self reportErrorWithCode:KBVoiceRecordManagerErrorRecorderFailed
message:@"Invalid file URL"];
return;
}
NSDictionary *settings = @{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVSampleRateKey : @(16000),
AVNumberOfChannelsKey : @(1),
AVEncoderAudioQualityKey : @(AVAudioQualityMedium),
AVEncoderBitRateKey : @(32000)
};
NSError *recorderError = nil;
self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL
settings:settings
error:&recorderError];
if (recorderError || !self.recorder) {
[self reportErrorWithCode:KBVoiceRecordManagerErrorRecorderFailed
message:recorderError.localizedDescription ?: @"Recorder error"];
return;
}
self.currentFileURL = fileURL;
self.recorder.delegate = self;
self.recorder.meteringEnabled = NO;
self.stopping = NO;
self.cancelled = NO;
self.interrupted = NO;
if (![self.recorder prepareToRecord] || ![self.recorder record]) {
[self reportErrorWithCode:KBVoiceRecordManagerErrorRecorderFailed
message:@"Recorder start failed"];
return;
}
self.recordStartTime = CACurrentMediaTime();
self.recording = YES;
if ([self.delegate respondsToSelector:@selector(voiceRecordManagerDidStartRecording:)]) {
[self.delegate voiceRecordManagerDidStartRecording:self];
}
}
- (BOOL)configureSession:(NSError **)error {
AVAudioSession *session = [AVAudioSession sharedInstance];
BOOL success =
[session setCategory:AVAudioSessionCategoryPlayAndRecord
mode:AVAudioSessionModeVoiceChat
options:(AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetooth)
error:error];
if (!success) {
return NO;
}
[session setPreferredSampleRate:16000 error:nil];
[session setPreferredIOBufferDuration:0.02 error:nil];
if ([session respondsToSelector:@selector(setPreferredInputNumberOfChannels:error:)]) {
[session setPreferredInputNumberOfChannels:1 error:nil];
}
return [session setActive:YES error:error];
}
- (NSURL *)generateFileURL {
NSString *fileName =
[NSString stringWithFormat:@"kb_record_%@.m4a",
@((NSInteger)([[NSDate date] timeIntervalSince1970] * 1000))];
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
return [NSURL fileURLWithPath:path];
}
- (void)cleanupRecorder {
self.recording = NO;
self.stopping = NO;
self.cancelled = NO;
self.interrupted = NO;
self.recordStartTime = 0;
self.recorder.delegate = nil;
self.recorder = nil;
self.currentFileURL = nil;
[[AVAudioSession sharedInstance] setActive:NO error:nil];
}
- (void)removeCurrentFileIfNeeded {
if (!self.currentFileURL) {
return;
}
[[NSFileManager defaultManager] removeItemAtURL:self.currentFileURL error:nil];
}
- (void)reportErrorWithCode:(KBVoiceRecordManagerErrorCode)code
message:(NSString *)message {
NSError *error = [NSError errorWithDomain:kKBVoiceRecordManagerErrorDomain
code:code
userInfo:@{NSLocalizedDescriptionKey : message ?: @""}];
if ([self.delegate respondsToSelector:@selector(voiceRecordManager:didFailWithError:)]) {
[self.delegate voiceRecordManager:self didFailWithError:error];
}
[self cleanupRecorder];
}
#pragma mark - Notifications
- (void)setupNotifications {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleInterruption:)
name:AVAudioSessionInterruptionNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleRouteChange:)
name:AVAudioSessionRouteChangeNotification
object:nil];
}
- (void)handleInterruption:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionInterruptionType type =
[info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (type == AVAudioSessionInterruptionTypeBegan && self.recording) {
self.interrupted = YES;
[self.recorder stop];
}
}
- (void)handleRouteChange:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionRouteChangeReason reason =
[info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable && self.recording) {
self.interrupted = YES;
[self.recorder stop];
}
}
#pragma mark - AVAudioRecorderDelegate
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder
successfully:(BOOL)flag {
if (!self.recording) {
[self cleanupRecorder];
return;
}
NSTimeInterval duration = recorder.currentTime;
if (duration <= 0 && self.recordStartTime > 0) {
duration = CACurrentMediaTime() - self.recordStartTime;
}
NSURL *fileURL = self.currentFileURL;
if (self.cancelled || !flag) {
[self removeCurrentFileIfNeeded];
if ([self.delegate respondsToSelector:@selector(voiceRecordManagerDidCancelRecording:)]) {
[self.delegate voiceRecordManagerDidCancelRecording:self];
}
[self cleanupRecorder];
return;
}
if (self.interrupted) {
[self removeCurrentFileIfNeeded];
[self reportErrorWithCode:KBVoiceRecordManagerErrorInterrupted
message:@"Audio session interrupted"];
return;
}
if (duration < self.minRecordDuration) {
[self removeCurrentFileIfNeeded];
if ([self.delegate respondsToSelector:@selector(voiceRecordManagerDidRecordTooShort:)]) {
[self.delegate voiceRecordManagerDidRecordTooShort:self];
}
[self cleanupRecorder];
return;
}
if (fileURL && [self.delegate respondsToSelector:@selector(voiceRecordManager:didFinishRecordingAtURL:duration:)]) {
[self.delegate voiceRecordManager:self didFinishRecordingAtURL:fileURL duration:duration];
}
[self cleanupRecorder];
}
@end

View File

@@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, weak) id<KBVoiceToTextManagerDelegate> delegate; @property(nonatomic, weak) id<KBVoiceToTextManagerDelegate> delegate;
@property(nonatomic, weak, readonly) KBVoiceInputBar *inputBar; @property(nonatomic, weak, readonly) KBVoiceInputBar *inputBar;
@property(nonatomic, assign) BOOL deepgramEnabled;
- (instancetype)initWithInputBar:(KBVoiceInputBar *)inputBar; - (instancetype)initWithInputBar:(KBVoiceInputBar *)inputBar;
- (void)prepareConnection; - (void)prepareConnection;

View File

@@ -20,12 +20,26 @@
@implementation KBVoiceToTextManager @implementation KBVoiceToTextManager
- (void)setDeepgramEnabled:(BOOL)deepgramEnabled {
if (_deepgramEnabled == deepgramEnabled) {
return;
}
_deepgramEnabled = deepgramEnabled;
if (!deepgramEnabled) {
[self.deepgramManager cancel];
[self resetTranscript];
} else {
[self.deepgramManager prepareConnection];
}
}
- (instancetype)initWithInputBar:(KBVoiceInputBar *)inputBar { - (instancetype)initWithInputBar:(KBVoiceInputBar *)inputBar {
self = [super init]; self = [super init];
if (self) { if (self) {
_inputBar = inputBar; _inputBar = inputBar;
_inputBar.delegate = self; _inputBar.delegate = self;
_fullText = [[NSMutableString alloc] init]; _fullText = [[NSMutableString alloc] init];
_deepgramEnabled = YES;
[self setupDeepgram]; [self setupDeepgram];
} }
return self; return self;
@@ -38,10 +52,16 @@
#pragma mark - Public Methods #pragma mark - Public Methods
- (void)prepareConnection { - (void)prepareConnection {
if (!self.deepgramEnabled) {
return;
}
[self.deepgramManager prepareConnection]; [self.deepgramManager prepareConnection];
} }
- (void)disconnect { - (void)disconnect {
if (!self.deepgramEnabled) {
return;
}
[self.deepgramManager disconnect]; [self.deepgramManager disconnect];
} }
@@ -70,8 +90,12 @@
- (void)voiceInputBarDidBeginRecording:(KBVoiceInputBar *)inputBar { - (void)voiceInputBarDidBeginRecording:(KBVoiceInputBar *)inputBar {
[self resetTranscript]; [self resetTranscript];
inputBar.statusText = @"正在连接..."; if (self.deepgramEnabled) {
[self.deepgramManager start]; inputBar.statusText = @"正在连接...";
[self.deepgramManager start];
} else {
inputBar.statusText = @"正在录音...";
}
if ([self.delegate respondsToSelector:@selector if ([self.delegate respondsToSelector:@selector
(voiceToTextManagerDidBeginRecording:)]) { (voiceToTextManagerDidBeginRecording:)]) {
@@ -80,8 +104,12 @@
} }
- (void)voiceInputBarDidEndRecording:(KBVoiceInputBar *)inputBar { - (void)voiceInputBarDidEndRecording:(KBVoiceInputBar *)inputBar {
inputBar.statusText = @"正在识别..."; if (self.deepgramEnabled) {
[self.deepgramManager stopAndFinalize]; inputBar.statusText = @"正在识别...";
[self.deepgramManager stopAndFinalize];
} else {
inputBar.statusText = @"录音结束";
}
if ([self.delegate respondsToSelector:@selector if ([self.delegate respondsToSelector:@selector
(voiceToTextManagerDidEndRecording:)]) { (voiceToTextManagerDidEndRecording:)]) {
@@ -92,7 +120,9 @@
- (void)voiceInputBarDidCancelRecording:(KBVoiceInputBar *)inputBar { - (void)voiceInputBarDidCancelRecording:(KBVoiceInputBar *)inputBar {
inputBar.statusText = @"已取消"; inputBar.statusText = @"已取消";
[self resetTranscript]; [self resetTranscript];
[self.deepgramManager cancel]; if (self.deepgramEnabled) {
[self.deepgramManager cancel];
}
if ([self.delegate respondsToSelector:@selector if ([self.delegate respondsToSelector:@selector
(voiceToTextManagerDidCancelRecording:)]) { (voiceToTextManagerDidCancelRecording:)]) {
@@ -103,10 +133,16 @@
#pragma mark - DeepgramStreamingManagerDelegate #pragma mark - DeepgramStreamingManagerDelegate
- (void)deepgramStreamingManagerDidConnect { - (void)deepgramStreamingManagerDidConnect {
if (!self.deepgramEnabled) {
return;
}
self.inputBar.statusText = @"正在聆听..."; self.inputBar.statusText = @"正在聆听...";
} }
- (void)deepgramStreamingManagerDidDisconnect:(NSError *_Nullable)error { - (void)deepgramStreamingManagerDidDisconnect:(NSError *_Nullable)error {
if (!self.deepgramEnabled) {
return;
}
if (!error) { if (!error) {
return; return;
} }
@@ -119,10 +155,16 @@
} }
- (void)deepgramStreamingManagerDidUpdateRMS:(float)rms { - (void)deepgramStreamingManagerDidUpdateRMS:(float)rms {
if (!self.deepgramEnabled) {
return;
}
[self.inputBar updateVolumeRMS:rms]; [self.inputBar updateVolumeRMS:rms];
} }
- (void)deepgramStreamingManagerDidReceiveInterimTranscript:(NSString *)text { - (void)deepgramStreamingManagerDidReceiveInterimTranscript:(NSString *)text {
if (!self.deepgramEnabled) {
return;
}
NSString *displayText = text ?: @""; NSString *displayText = text ?: @"";
if (self.fullText.length > 0 && displayText.length > 0) { if (self.fullText.length > 0 && displayText.length > 0) {
displayText = displayText =
@@ -141,6 +183,9 @@
} }
- (void)deepgramStreamingManagerDidReceiveFinalTranscript:(NSString *)text { - (void)deepgramStreamingManagerDidReceiveFinalTranscript:(NSString *)text {
if (!self.deepgramEnabled) {
return;
}
if (text.length > 0) { if (text.length > 0) {
if (self.fullText.length > 0) { if (self.fullText.length > 0) {
[self.fullText appendString:@" "]; [self.fullText appendString:@" "];
@@ -160,6 +205,9 @@
} }
- (void)deepgramStreamingManagerDidFail:(NSError *)error { - (void)deepgramStreamingManagerDidFail:(NSError *)error {
if (!self.deepgramEnabled) {
return;
}
self.inputBar.statusText = @"识别失败"; self.inputBar.statusText = @"识别失败";
if ([self.delegate respondsToSelector:@selector if ([self.delegate respondsToSelector:@selector
(voiceToTextManager:didFailWithError:)]) { (voiceToTextManager:didFailWithError:)]) {