// // AiVM.m // keyBoard // // Created by Mac on 2026/1/22. // #import "AiVM.h" #import "KBAPI.h" #import "KBNetworkManager.h" #import @implementation KBAiSyncData - (void)setAudioBase64:(NSString *)audioBase64 { if (![audioBase64 isKindOfClass:[NSString class]]) { _audioBase64 = nil; self.audioData = nil; return; } _audioBase64 = [audioBase64 copy]; if (_audioBase64.length == 0) { self.audioData = nil; return; } NSString *cleanBase64 = _audioBase64; NSRange commaRange = [cleanBase64 rangeOfString:@","]; if ([cleanBase64 hasPrefix:@"data:"] && commaRange.location != NSNotFound) { cleanBase64 = [cleanBase64 substringFromIndex:commaRange.location + 1]; } self.audioData = [[NSData alloc] initWithBase64EncodedString:cleanBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]; } @end @implementation KBAiSyncResponse @end @implementation KBAiMessageData @end @implementation KBAiMessageResponse @end @implementation AiVM - (void)syncChatWithTranscript:(NSString *)transcript completion:(AiVMSyncCompletion)completion { if (transcript.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"transcript is empty"}]; if (completion) { completion(nil, error); } return; } NSDictionary *params = @{ @"transcript" : transcript ?: @"" }; CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); NSLog(@"[AiVM] /chat/sync request: %@", params); [[KBNetworkManager shared] POST:API_AI_CHAT_SYNC jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { CFAbsoluteTime elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0; if (error) { NSLog(@"[AiVM] /chat/sync failed: %@", error.localizedDescription ?: @""); NSLog(@"[AiVM] /chat/sync duration: %.0f ms", elapsed); if (completion) { completion(nil, error); } return; } NSLog(@"[AiVM] /chat/sync response received"); NSLog(@"[AiVM] /chat/sync duration: %.0f ms", elapsed); KBAiSyncResponse *model = [KBAiSyncResponse mj_objectWithKeyValues:json]; if (completion) { completion(model, nil); } }]; } - (void)requestChatMessageWithContent:(NSString *)content completion:(AiVMMessageCompletion)completion { if (content.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"content is empty"}]; if (completion) { completion(nil, error); } return; } NSString *encodedContent = [content stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]]; NSString *path = [NSString stringWithFormat:@"%@?content=%@", API_AI_CHAT_MESSAGE, encodedContent ?: @""]; NSDictionary *params = @{ @"content" : content ?: @"" }; [[KBNetworkManager shared] POST:path jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary *_Nullable json, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { if (completion) { completion(nil, error); } return; } KBAiMessageResponse *model = [KBAiMessageResponse mj_objectWithKeyValues:json]; if (completion) { completion(model, nil); } }]; } - (void)requestElevenLabsSpeechWithText:(NSString *)text voiceId:(NSString *)voiceId apiKey:(NSString *)apiKey outputFormat:(NSString *)outputFormat modelId:(NSString *)modelId completion:(AiVMElevenLabsCompletion)completion { if (text.length == 0 || voiceId.length == 0 || apiKey.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"invalid parameters"}]; if (completion) { completion(nil, error); } return; } NSString *format = outputFormat.length > 0 ? outputFormat : @"mp3_44100_128"; NSString *model = modelId.length > 0 ? modelId : @"eleven_multilingual_v2"; NSString *escapedVoiceId = [voiceId stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLPathAllowedCharacterSet]]; NSString *escapedFormat = [format stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]]; NSString *urlString = [NSString stringWithFormat:@"https://api.elevenlabs.io/v1/text-to-speech/%@/stream?output_format=%@", escapedVoiceId ?: @"", escapedFormat ?: @""]; NSURL *url = [NSURL URLWithString:urlString]; if (!url) { NSError *error = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"invalid URL"}]; if (completion) { completion(nil, error); } return; } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request setValue:@"audio/mpeg" forHTTPHeaderField:@"Accept"]; [request setValue:apiKey forHTTPHeaderField:@"xi-api-key"]; NSDictionary *body = @{ @"text" : text ?: @"", @"model_id" : model ?: @"" }; NSError *jsonError = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:body options:0 error:&jsonError]; if (jsonError) { if (completion) { completion(nil, jsonError); } return; } request.HTTPBody = jsonData; NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (error) { if (completion) { completion(nil, error); } return; } if (![response isKindOfClass:[NSHTTPURLResponse class]]) { NSError *respError = [NSError errorWithDomain:@"AiVM" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"invalid response" }]; if (completion) { completion(nil, respError); } return; } NSInteger status = ((NSHTTPURLResponse *)response).statusCode; if (status < 200 || status >= 300 || data.length == 0) { NSError *respError = [NSError errorWithDomain:@"AiVM" code:status userInfo:@{ NSLocalizedDescriptionKey : @"request failed" }]; if (completion) { completion(nil, respError); } return; } if (completion) { completion(data, nil); } }]; [task resume]; } @end