// // TTSServiceClient.m // keyBoard // // Created by Mac on 2026/1/15. // #import "TTSServiceClient.h" @interface TTSServiceClient () @property(nonatomic, strong) NSURLSession *urlSession; @property(nonatomic, strong) NSMutableDictionary *activeTasks; @property(nonatomic, strong) dispatch_queue_t networkQueue; @property(nonatomic, assign) BOOL requesting; @end @implementation TTSServiceClient - (instancetype)init { self = [super init]; if (self) { _networkQueue = dispatch_queue_create("com.keyboard.aitalk.tts.network", DISPATCH_QUEUE_SERIAL); _activeTasks = [[NSMutableDictionary alloc] init]; _expectedPayloadType = TTSPayloadTypeURL; // 默认 URL 模式 // TODO: 替换为实际的 TTS 服务器地址 _serverURL = @"https://your-tts-server.com/api/tts"; [self setupSession]; } return self; } - (void)setupSession { NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.timeoutIntervalForRequest = 30; config.timeoutIntervalForResource = 120; self.urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; } - (void)dealloc { [self cancel]; } #pragma mark - Public Methods - (void)requestTTSForText:(NSString *)text segmentId:(NSString *)segmentId { if (!text || text.length == 0 || !segmentId) { return; } dispatch_async(self.networkQueue, ^{ self.requesting = YES; switch (self.expectedPayloadType) { case TTSPayloadTypeURL: [self requestURLMode:text segmentId:segmentId]; break; case TTSPayloadTypePCMChunk: case TTSPayloadTypeAACChunk: case TTSPayloadTypeOpusChunk: [self requestStreamMode:text segmentId:segmentId]; break; } }); } - (void)cancel { dispatch_async(self.networkQueue, ^{ for (NSURLSessionTask *task in self.activeTasks.allValues) { [task cancel]; } [self.activeTasks removeAllObjects]; self.requesting = NO; }); } #pragma mark - URL Mode (Mode A) - (void)requestURLMode:(NSString *)text segmentId:(NSString *)segmentId { NSURL *url = [NSURL URLWithString:self.serverURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; NSDictionary *body = @{ @"text" : text, @"segmentId" : segmentId, @"format" : @"mp3" // 或 m4a }; NSError *jsonError = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:body options:0 error:&jsonError]; if (jsonError) { [self reportError:jsonError]; return; } request.HTTPBody = jsonData; __weak typeof(self) weakSelf = self; NSURLSessionDataTask *task = [self.urlSession dataTaskWithRequest:request completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; dispatch_async(strongSelf.networkQueue, ^{ [strongSelf.activeTasks removeObjectForKey:segmentId]; if (error) { if (error.code != NSURLErrorCancelled) { [strongSelf reportError:error]; } return; } // 解析响应 NSError *parseError = nil; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError]; if (parseError) { [strongSelf reportError:parseError]; return; } NSString *audioURLString = json[@"audioUrl"]; if (audioURLString) { NSURL *audioURL = [NSURL URLWithString:audioURLString]; dispatch_async(dispatch_get_main_queue(), ^{ if ([strongSelf.delegate respondsToSelector:@selector (ttsClientDidReceiveURL:segmentId:)]) { [strongSelf.delegate ttsClientDidReceiveURL:audioURL segmentId:segmentId]; } if ([strongSelf.delegate respondsToSelector:@selector (ttsClientDidFinishSegment:)]) { [strongSelf.delegate ttsClientDidFinishSegment:segmentId]; } }); } }); }]; self.activeTasks[segmentId] = task; [task resume]; NSLog(@"[TTSServiceClient] URL mode request for segment: %@", segmentId); } #pragma mark - Stream Mode (Mode B/C/D) - (void)requestStreamMode:(NSString *)text segmentId:(NSString *)segmentId { // WebSocket 连接用于流式接收 NSString *wsURL = [self.serverURL stringByReplacingOccurrencesOfString:@"https://" withString:@"wss://"]; wsURL = [wsURL stringByReplacingOccurrencesOfString:@"http://" withString:@"ws://"]; wsURL = [wsURL stringByAppendingString:@"/stream"]; NSURL *url = [NSURL URLWithString:wsURL]; NSURLSessionWebSocketTask *wsTask = [self.urlSession webSocketTaskWithURL:url]; self.activeTasks[segmentId] = wsTask; [wsTask resume]; // 发送请求 NSDictionary *requestDict = @{ @"text" : text, @"segmentId" : segmentId, @"format" : [self formatStringForPayloadType:self.expectedPayloadType] }; NSError *jsonError = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:requestDict options:0 error:&jsonError]; if (jsonError) { [self reportError:jsonError]; return; } NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; NSURLSessionWebSocketMessage *message = [[NSURLSessionWebSocketMessage alloc] initWithString:jsonString]; __weak typeof(self) weakSelf = self; [wsTask sendMessage:message completionHandler:^(NSError *_Nullable error) { if (error) { [weakSelf reportError:error]; } else { [weakSelf receiveStreamMessage:wsTask segmentId:segmentId]; } }]; NSLog(@"[TTSServiceClient] Stream mode request for segment: %@", segmentId); } - (void)receiveStreamMessage:(NSURLSessionWebSocketTask *)wsTask segmentId:(NSString *)segmentId { __weak typeof(self) weakSelf = self; [wsTask receiveMessageWithCompletionHandler:^( NSURLSessionWebSocketMessage *_Nullable message, NSError *_Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; if (error) { if (error.code != NSURLErrorCancelled && error.code != 57) { [strongSelf reportError:error]; } return; } if (message.type == NSURLSessionWebSocketMessageTypeData) { // 音频数据块 dispatch_async(dispatch_get_main_queue(), ^{ if ([strongSelf.delegate respondsToSelector:@selector (ttsClientDidReceiveAudioChunk: payloadType:segmentId:)]) { [strongSelf.delegate ttsClientDidReceiveAudioChunk:message.data payloadType:strongSelf.expectedPayloadType segmentId:segmentId]; } }); // 继续接收 [strongSelf receiveStreamMessage:wsTask segmentId:segmentId]; } else if (message.type == NSURLSessionWebSocketMessageTypeString) { // 控制消息 NSData *data = [message.string dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if ([json[@"type"] isEqualToString:@"done"]) { dispatch_async(strongSelf.networkQueue, ^{ [strongSelf.activeTasks removeObjectForKey:segmentId]; }); dispatch_async(dispatch_get_main_queue(), ^{ if ([strongSelf.delegate respondsToSelector:@selector(ttsClientDidFinishSegment:)]) { [strongSelf.delegate ttsClientDidFinishSegment:segmentId]; } }); } else { // 继续接收 [strongSelf receiveStreamMessage:wsTask segmentId:segmentId]; } } }]; } - (NSString *)formatStringForPayloadType:(TTSPayloadType)type { switch (type) { case TTSPayloadTypePCMChunk: return @"pcm"; case TTSPayloadTypeAACChunk: return @"aac"; case TTSPayloadTypeOpusChunk: return @"opus"; default: return @"mp3"; } } #pragma mark - Error Reporting - (void)reportError:(NSError *)error { self.requesting = NO; dispatch_async(dispatch_get_main_queue(), ^{ if ([self.delegate respondsToSelector:@selector(ttsClientDidFail:)]) { [self.delegate ttsClientDidFail:error]; } }); } @end