Files
keyboard/keyBoard/Class/AiTalk/VM/TTSServiceClient.m
2026-01-21 17:25:38 +08:00

303 lines
9.7 KiB
Objective-C

//
// TTSServiceClient.m
// keyBoard
//
// Created by Mac on 2026/1/15.
//
#import "TTSServiceClient.h"
@interface TTSServiceClient () <NSURLSessionDataDelegate,
NSURLSessionWebSocketDelegate>
@property(nonatomic, strong) NSURLSession *urlSession;
@property(nonatomic, strong)
NSMutableDictionary<NSString *, NSURLSessionTask *> *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,
@"voiceId" : self.voiceId ?: @"JBFqnCBsd6RMkjVDRZzb",
@"languageCode" : self.languageCode ?: @"zh",
@"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,
@"voiceId" : self.voiceId ?: @"JBFqnCBsd6RMkjVDRZzb",
@"languageCode" : self.languageCode ?: @"zh",
@"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