添加语音websocket等,还没测试
This commit is contained in:
298
keyBoard/Class/AiTalk/VM/TTSServiceClient.m
Normal file
298
keyBoard/Class/AiTalk/VM/TTSServiceClient.m
Normal file
@@ -0,0 +1,298 @@
|
||||
//
|
||||
// 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,
|
||||
@"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
|
||||
Reference in New Issue
Block a user