256 lines
8.3 KiB
Objective-C
256 lines
8.3 KiB
Objective-C
//
|
|
// AiVM.m
|
|
// keyBoard
|
|
//
|
|
// Created by Mac on 2026/1/22.
|
|
//
|
|
|
|
#import "AiVM.h"
|
|
#import "KBAPI.h"
|
|
#import "KBNetworkManager.h"
|
|
#import <MJExtension/MJExtension.h>
|
|
|
|
@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
|