集成阿里云 NUI 语音合成 SDK

This commit is contained in:
2026-07-07 16:05:39 +08:00
parent 0aa8b14e1f
commit 9378748a4d
26 changed files with 2095 additions and 16 deletions

View File

@ -0,0 +1,210 @@
#ifndef NeoNui_h
#define NeoNui_h
#ifdef DEBUG_MODE
#define TLog( s, ... ) NSLog( s, ##__VA_ARGS__ )
#else
#define TLog( s, ... )
#endif
#import <Foundation/Foundation.h>
#import "NeoNuiCode.h"
typedef int NuiResultCode;
enum NuiAudioState {
STATE_OPEN,
STATE_PAUSE,
STATE_CLOSE,
};
enum NuiFunMode {
kFuncModeAsp = 0x00000001,
kFuncModeVad = 0x00000010,
kFuncModeKws = 0x00000100,
kFuncModeAsr = 0x00001000,
kFuncModeEnc = 0x00010000,
kFuncModeOu = 0x00100000,
kFuncModeFull = 0x11111111,
};
enum NuiAudioExtraEvent {
AudioRmsChanged,
};
enum NuiCallbackEvent {
EVENT_VAD_START,
EVENT_VAD_TIMEOUT,
EVENT_VAD_END,
EVENT_WUW,
EVENT_WUW_TRUSTED,
EVENT_WUW_CONFIRMED,
EVENT_WUW_REJECTED,
EVENT_WUW_END,
EVENT_ASR_PARTIAL_RESULT,
EVENT_ASR_RESULT,
EVENT_ASR_ERROR, // 10
EVENT_DIALOG_ERROR,
EVENT_ONESHOT_TIMEOUT,
EVENT_DIALOG_RESULT,
EVENT_WUW_HINT,
EVENT_VPR_RESULT,
EVENT_TEXT2ACTION_DIALOG_RESULT,
EVENT_TEXT2ACTION_ERROR,
EVENT_ATTR_RESULT,
EVENT_MIC_ERROR,
EVENT_DIALOG_EX, // 20
EVENT_WUW_ERROR,
EVENT_BEFORE_CONNECTION,
EVENT_SENTENCE_START,
EVENT_SENTENCE_END,
EVENT_SENTENCE_SEMANTICS,
EVENT_RESULT_TRANSLATED,
EVENT_TRANSCRIBER_COMPLETE,
EVENT_FILE_TRANS_CONNECTED,
EVENT_FILE_TRANS_UPLOADED,
EVENT_FILE_TRANS_RESULT, // 30
EVENT_FILE_TRANS_UPLOAD_PROGRESS,
EVENT_TRANSCRIBER_STARTED,
EVENT_ASR_STARTED,
EVENT_FILE_TRANS_QUERY_RESULT,
EVENT_WUW_START,
EVENT_WUW_DATA,
EVENT_ASR_TTS_DATA,
EVENT_ASR_TTS_START,
EVENT_ASR_TTS_COMPLETE,
EVENT_RESULT_TRANSLATED_END,
EVENT_ASR_WARN, // 41
};
enum NuiVadMode {
MODE_VAD,
MODE_P2T,
MODE_KWS,
MODE_PARALLEL,
MODE_KWS2PARALLEL,
MODE_AUTO_CONTINUAL,
MODE_KWS_CONTINUAL,
MODE_KWS2TALK,
MODE_ONLY_KWS
};
enum ServiceType {
SERVICE_TYPE_NONE = -1,
SERVICE_TYPE_ASR = 0, // SpeechRecognizer
SERVICE_TYPE_TIANGONG_ASSISTANT = 1,
SERVICE_TYPE_DIALOG_ASSISTANT = 2, //DialogAssistant
SERVICE_TYPE_VIRTUAL_ASSISTANT = 3, //TiangongV4
SERVICE_TYPE_SPEECH_TRANSCRIBER = 4 //SpeechTranscriber
};
enum NuiSdkVprEvent {
EVENT_VPR_NONE,
EVENT_VPR_REGISTER_START,
EVENT_VPR_REGISTER_DONE,
EVENT_VPR_REGISTER_FAILED,
EVENT_VPR_UPDATE_START,
EVENT_VPR_UPDATE_DONE,
EVENT_VPR_UPDATE_FAIL,
EVENT_VPR_DELETE_DONE,
EVENT_VPR_DELETE_FAIL
};
typedef enum NuiSdkVprEvent NuiSdkVprEvent;
typedef enum NuiAudioState NuiAudioState;
typedef enum NuiCallbackEvent NuiCallbackEvent;
typedef enum NuiCallbackEvent NuiCallbackEvent;
typedef enum NuiVadMode NuiVadMode;
@protocol NeoNuiSdkDelegate <NSObject>
@optional
-(int) onNuiNeedAudioData:(char *)audioData length:(int)len;
-(void) onNuiAudioStateChanged:(NuiAudioState)state;
-(void) onNuiEventCallback:(NuiCallbackEvent)nuiEvent
dialog:(long)dialog
kwsResult:(const char *)wuw
asrResult:(const char *)asr_result
ifFinish:(BOOL)finish
retCode:(int)code;
-(void) onNuiRmsChanged:(float) rms;
-(void) onNuiAssistEventCallback:(NuiCallbackEvent)nuiEvent
info:(char*)info
infoLen:(int)info_len
buffer:(char*)buffer
len:(int)len;
-(void) onNuiLogTrackCallback:(NuiSdkLogLevel)level
logMessage:(const char *)log;
-(void) onFileTransEventCallback:(NuiCallbackEvent)nuiEvent
asrResult:(const char *)asr_result
taskId:(const char *)task_id
ifFinish:(BOOL)finish
retCode:(int)code;
-(void) onFileTransLogTrackCallback:(NuiSdkLogLevel)level
logMessage:(const char *)log;
@end
@interface NeoNui : NSObject
@property (readonly) void* nui_sdk;
@property (nonatomic,weak) id<NeoNuiSdkDelegate> delegate;
+ (instancetype)get_instance;
//Initialize API
-(NuiResultCode) nui_initialize:(const char *)parameters
logLevel:(NuiSdkLogLevel)level
saveLog:(BOOL)save_log;
//Release API
-(NuiResultCode) nui_release;
//Dialog API
-(NuiResultCode) nui_dialog_start:(NuiVadMode)vad_mode
dialogParam:(const char *)dialog_params;
-(NuiResultCode) nui_dialog_cancel:(BOOL)force;
-(NuiResultCode) nui_dialog_resume;
//Text to Action API
-(NuiResultCode) nui_dialog_text2action:(const char *)text
Context:(const char *)context
isNewDialog:(BOOL)is_new_dialog
dialogParams:(const char *)dialog_params;
-(NuiResultCode) nui_dialog_text2action_cancel;
//File Transcriber API
-(NuiResultCode) nui_file_trans_start:(const char *)params
taskId:(char*)task_id;
-(NuiResultCode) nui_file_trans_cancel:(const char*)task_id;
-(NuiResultCode) nui_file_trans_query:(const char*)task_id;
//Set parameter API
-(NuiResultCode) nui_set_param:(const char *)para
Value:(const char *)value;
-(NuiResultCode) nui_set_params:(const char *)params;
-(const char *) nui_get_param:(const char *)param;
-(NuiResultCode) nui_update_audio_data:(const char *)data
Len:(int)length
FirstPack:(BOOL)first_pack;
-(const char*) nui_get_version;
-(const char*) nui_get_all_response;
#ifdef NUI_VPR
//Voice Print Recognition API
-(NuiResultCode) nui_vpr_register_user:(const char*)service_id
groupId:(const char * )group_id
userId:(const char * )user_id;
-(NuiResultCode) nui_vpr_update_user:(const char* )service_id
groupId:(const char * )group_id
userId:(const char * )user_id;
-(NuiResultCode) nui_vpr_delete_user:(const char* )service_id
groupId:(const char * )group_id
userId:(const char * )user_id;
-(NuiResultCode) nui_vpr_enable;
-(NuiResultCode) nui_vpr_disable;
-(NuiResultCode) nui_vpr_register_cancel;
#endif
@end
#endif

View File

@ -0,0 +1,138 @@
//
// NeoNuiCode.h
//
//
// Created by zhouguangdong on 2021/3/11.
//
#ifndef NeoNuiCode_h
#define NeoNuiCode_h
enum NuiSdkLogLevel {
NUI_LOG_LEVEL_VERBOSE,
NUI_LOG_LEVEL_DEBUG,
NUI_LOG_LEVEL_INFO,
NUI_LOG_LEVEL_WARNING,
NUI_LOG_LEVEL_ERROR,
NUI_LOG_LEVEL_NONE,
};
typedef enum NuiSdkLogLevel NuiSdkLogLevel;
typedef int NuiResultCode;
#define SUCCESS (0)
//legacy, not required handle
#define DEFAULT_ERROR (240999) //default err value.
//config or params invalid
#define NUI_CONFIG_INVALID (240001) //Config or file invalid.
#define ILLEGAL_PARAM (240002) //parmeters illegal.
#define ILLEGAL_INIT_PARAM (240003) //initialize with illegal parameters.
#define NECESSARY_PARAM_LACK (240004) //lack of necessaray parameters.
#define NULL_PARAM_ERROR (240005) //parameters with null pointer or empty content.
#define NULL_LISTENER_ERROR (240006) //no listener for callback set.
#define NULL_DIALOG_ERROR (240007) //no dialog created.
#define NULL_ENGINE_ERROR (240008) //no dialog engine created.
#define ILLEGAL_DATA (240009) //transfer illegal audio or video data.
#define ILLEGAL_WORD_LIST (2400010) //word list format error
//state invalid
#define ILLEGAL_REENTRANT (240010) //happen when call from async callback or sdk already exit.
#define SDK_NOT_INIT (240011) //call apis when sdk not initialized.
#define SDK_ALREADY_INIT (240012) //call initialize if already initialized.
#define DIALOG_INVALID_STATE (240013) //dialog state machine error.
#define STATE_INVALID (240014) //asr engine state machine error.
#define ILLEGAL_FUNC_CALL (240015) //some function is not supposed to call on some working mode.
//system call fail
#define MEM_ALLOC_ERROR (240020) //alloc or new object fail.
#define FILE_ACCESS_FAIL (240021) //access file fail.
#define CREATE_DIR_ERROR (240022) //create directory fail.
//nui call fail
#define CREATE_NUI_ERROR (240030) //create dialog engine fail.
#define TEXT_DIALOG_START_FAIL (240031) //start text to action fail.
#define TEXT_DIALOG_CANCEL_FAIL (240032) //cancel text to action fail.
#define WUW_DUPLICATE (240033) //set duplicated wake words for dynamic keywords.
//cei call fail
#define CEI_INIT_FAIL (240040) //init cei engine fail.
#define CEI_SET_PARAM_FAIL (240041) //cei set parameters fail.
#define CEI_COMPILE_GRAMMAR_FAIL (240042) //cei compile grammar fail.
#define CEI_STOP_FAIL (240043) //cei stop fail.
#define CEI_CANCEL_FAIL (240044) //cei cancel fail.
#define CEI_UNLOAD_KWS_FAIL (240045) //cei unload kws fail.
#define GET_WUW_ERROR (240046) //cei get wakeup word fail.
#define CEI_CHECK_BIN_FAIL (240047) //check resource bin file fail
//audio manager
#define SELECT_RECORDER_ERROR (240050) //select recorder fail.
#define UPDATE_AUDIO_ERROR (240051) //update audio fail) usually push audio more than wanted.
#define MIC_ERROR (240052) //get audio data fail continually for 2 senconds.
//nls error
#define CREATE_DA_REQUEST_ERROR (240060) //create dialog assistant fail.
#define START_DA_REQUEST_ERROR (240061) //start dialog assistant fail.
#define DEFAULT_NLS_ERROR (240062) //default network error.
#define SSL_ERROR (240063) //ssl new fail.
#define SSL_CONNECT_FAILED (240064) //ssl connect fail.
#define HTTP_CONNECT_FAILED (240065) //http connect fail.
#define DNS_FAILED (240066) //DNS fail.
#define CONNECT_FAILED (240067) //connect socket fail.
#define SERVER_NOT_ACCESS (240068) //server responce status bad.
#define SOCKET_CLOSED (240069) //socket closed.
#define AUTH_FAILED (240070) //authentication fail.
#define HTTPDNS_FAILED (240071) //connect fail when use http dns.
#define HTTP_SEND_FAILED (240072)
#define HTTP_RECEIVE_FAILED (240073)
#define HTTP_RESPONSE_ERROR (240074)
#define HTTP_SERVER_ERROR (240075)
//function call timeout
#define ENGINE_INIT_TIMEOUT (240080) //init engine timeout.
#define SET_PARAM_TIMEOUT (240081) //set parameters timeout.
#define SET_WUW_TIMEOUT (240082) //set wakeup word timeout.
#define SELECT_RECORDER_TIMEOUT (240083) //select recorder timeout.
#define STOP_TIMEOUT (240084) //stop dialog timeout.
#define ASR_ENGINE_STOP_TIMEOUT (240085) //asr engine stop timeout.
#define UNLOAD_DYNAMIC_WUW_TIMEOUT (240086) //unload dynamic wakeup word timeout.
#define ADD_DYNAMIC_WUW_TIMEOUT (240087) //add dynamic wakeup word timeout.
#define HANDLE_API_TIMEOUT (240088)
#define CHECK_ASSET_TIMEOUT (240089) //check asset bin file timeout
//network timeout
#define UPDATE_CONTEXT_TIMEOUT (240090) //update context timeout.
#define CONNECTION_TIMEOUT (240091) //connect to server timeout.
#define PARTIAL_ASR_TIMEOUT (240092) //get partial asr result timeout.
#define ASR_TIMEOUT (240093) //get final asr result timeout.
#define DIALOG_TIMEOUT (240094) //get dialog result timeout.
#define WWV_TIMEOUT (240095) //get wake word verification result timeout.
//legacy
#define WAIT_TIMEOUT (240100) //legacy definition.
#ifdef NUI_VIDEO_MANAGER
#define SELECT_CAMERA_TIMEOUT (240101)
#define SELECT_CAMERA_ERROR (240102)
#endif
#ifdef NUI_SENSOR_MANAGER
#define SELECT_SENSOR_TIMEOUT (240103)
#define SELECT_SENSOR_ERROR (240104)
#endif
#ifdef NUI_INCLUDE_VPR
#define VPR_ARGS_INVALID (240110)
#define VPR_REG_TIMEOUT (240111)
#define VPR_STATE_INVALID (240112)
#define VPR_UPDATE_TIMEOUT (240113)
#define VPR_DELETE_TIMEOUT (240114)
#define VPR_CANCEL_TIMEOUT (240115)
#endif
#define RING_BUF_WRITE_FAIL (240120)
#define AUDIO_PROCESS_THREAD_BLOCK (240121)
#define PERFORMANCE_FILEOVER (240130)
#define RESAMPLE_ERR (240140)
#define FILE_TRANS_TASK_LIMIT (240150)
#define FILE_TRANS_ENCODER_FAIL (140151)
#define THREAD_CREATE_FAILD (240200)
#define SERVER_ERR_SILENT_SPEECH (40010007)
#endif /* NeoNuiCode_h */

View File

@ -0,0 +1,84 @@
//
// NeoNuiTts.h
// nuisdk
//
// Created by zhouguangdong on 2019/12/21.
// Copyright © 2019年 zhouguangdong. All rights reserved.
//
#ifndef NeoNuiTts_h
#define NeoNuiTts_h
#ifdef DEBUG_MODE
#define TLog( s, ... ) NSLog( s, ##__VA_ARGS__ )
#else
#define TLog( s, ... )
#endif
#import <Foundation/Foundation.h>
#import "NeoNuiCode.h"
enum NuiSdkTtsEvent {
TTS_EVENT_START = 0,
TTS_EVENT_END = 1,
TTS_EVENT_CANCEL = 2,
TTS_EVENT_PAUSE = 3,
TTS_EVENT_RESUME = 4,
TTS_EVENT_ERROR = 5,
TTS_EVENT_CACEH_START = 6,
TTS_EVENT_CACEH_END = 7,
TTS_EVENT_CACEH_CANCEL = 8,
TTS_EVENT_CACEH_DELETE = 9,
TTS_EVENT_CACEH_ERROR = 10,
TTS_EVENT_FONT_EVENT_START = 11,
TTS_EVENT_FONT_DOWNLOAD = 12,
TTS_EVENT_FONT_END = 13,
TTS_EVENT_FONT_PAUSE = 14,
TTS_EVENT_FONT_RESUME = 15,
TTS_EVENT_FONT_CANCEL = 16,
TTS_EVENT_FONT_ERROR = 17
};
typedef enum NuiSdkTtsEvent NuiSdkTtsEvent;
@protocol NeoNuiTtsDelegate <NSObject>
@optional
- (void)onNuiTtsEventCallback:(NuiSdkTtsEvent)event taskId:(char*)taskid code:(int)code;
- (void)onNuiTtsUserdataCallback:(char*)info infoLen:(int)info_len buffer:(char*)buffer len:(int)len taskId:(char*)task_id;
- (void)onNuiTtsVolumeCallback:(int)volume taskId:(char*)task_id;
- (void)onNuiTtsLogTrackCallback:(NuiSdkLogLevel)level
logMessage:(const char *)log;
@end
@interface NeoNuiTts : NSObject
@property (readonly) void* nui_tts_sdk;
@property (nonatomic,weak) id<NeoNuiTtsDelegate> delegate;
+ (instancetype)get_instance;
-(int) nui_tts_initialize:(const char *)parameters
logLevel:(NuiSdkLogLevel)level
saveLog:(BOOL)save_log;
-(int) nui_tts_release;
-(int) nui_tts_play:(const char *)priority
taskId:(const char *)taskid
text:(const char *)text;
-(int) nui_tts_cancel:(const char *)taskid;
-(int) nui_tts_pause;
-(int) nui_tts_resume;
-(int) nui_tts_set_param:(const char *)param
value:(const char *)value;
-(const char *) nui_tts_get_param:(const char *)param;
-(int) nui_tts_get_num_of_chars:(const char *)param;
@end
#endif /* NeoNuiTts_h */

View File

@ -0,0 +1,43 @@
#ifndef NeoNuiUtils_h
#define NeoNuiUtils_h
#ifdef DEBUG_MODE
#define TLog( s, ... ) NSLog( s, ##__VA_ARGS__ )
#else
#define TLog( s, ... )
#endif
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface NeoNuiUtils : NSObject
@property (readonly) void* nui_utils;
+ (instancetype)get_instance;
- (const char *)nui_utils_refresh_apikey:(const char *)apikey
Url:(const char *)url;
- (const char *)nui_uitls_get_version;
- (const char *)nui_uitls_get_app_code;
+ (NSString *)replenishHardwareInfo:(NSString *)params;
+ (NSDictionary *)hardwareInfo;
+ (NSString*)getDeviceType;
+ (NSString *)getDeviceModel;
+ (NSString*)getDeviceName;
@end
NS_ASSUME_NONNULL_END
#endif /* NeoNuiUtils_h */

View File

@ -0,0 +1,131 @@
//
// NeoNuiStreamInputTts.h
// nuisdk
//
// Created by lengjiayi on 2024/4/17.
// Copyright © 2024 zhouguangdong. All rights reserved.
//
#ifndef StreamInputTts_h
#define StreamInputTts_h
#import <Foundation/Foundation.h>
#import "NeoNuiCode.h"
enum StreamInputTtsCallbackEvent {
TTS_EVENT_SYNTHESIS_STARTED = 0,
TTS_EVENT_SENTENCE_BEGIN = 1,
TTS_EVENT_SENTENCE_SYNTHESIS = 2,
TTS_EVENT_SENTENCE_END = 3,
TTS_EVENT_SYNTHESIS_COMPLETE = 4,
TTS_EVENT_TASK_FAILED = 5
};
typedef enum StreamInputTtsCallbackEvent StreamInputTtsCallbackEvent;
@protocol StreamInputTtsDelegate <NSObject>
@optional
/**
* 事件回调
* @param event 回调事件,参见如下事件列表。
* @param taskid 请求的任务ID每次调用一个新ID。
* @param sessionId 请求的会话ID。
* @param ret_code 参见错误码出现TTS_EVENT_ERROR事件时有效可查阅xxxxx。
* @param error_msg 当产生错误码时,返回错误信息。
* @param timestamp 时间戳信息。
* @param all_response 返回的完整json格式信息。
*/
- (void)onStreamInputTtsEventCallback:(StreamInputTtsCallbackEvent)event taskId:(char*)taskid
sessionId:(char*)sessionId ret_code:(int)ret_code
error_msg:(char*)error_msg timestamp:(char*)timestamp all_response:(char*)all_response;
/**
* 当合成数据时此回调被连续调用App需要在回调中获得合成的音频数据
* @param buffer 合成的语音数据
* @param len 合成的语音长度
*/
- (void)onStreamInputTtsDataCallback:(char*)buffer len:(int)len;
- (void)onStreamInputTtsLogTrackCallback:(NuiSdkLogLevel)level
logMessage:(const char *)log;
@end
@interface StreamInputTts : NSObject
@property (nonatomic,weak) id<StreamInputTtsDelegate> delegate;
+ (instancetype)get_instance;
/**
* 与服务端完成建链,并开始流式语音合成任务
* @param ticket json string形式的鉴权参数参见下方说明或接口说明xxxxx
* @param parameters json string形式的初始化配置参数参见下方说明或接口说明xxxxx
* @param sessionId 会话ID可传入32个字节的uuid或传入空内容由SDK自动生成。
* @param logLevel log打印级别值越小打印越多。
* @param saveLog 是否保存log为文件存储目录为ticket中的debug_path字段值。
* @return参见错误码https://help.aliyun.com/document_detail/459864.html。
*/
- (int) startStreamInputTts:(const char *)ticket parameters:(const char *)parameters
sessionId:(const char *)sessionId logLevel:(NuiSdkLogLevel)logLevel
saveLog:(BOOL)saveLog;
/**
* 与服务端完成建链,并开始单向流式语音合成(长文本语音合成)任务,同步接口
* @param ticket json string形式的鉴权参数参见下方说明或接口说明xxxxx
* @param parameters json string形式的初始化配置参数参见下方说明或接口说明xxxxx
* @param text 待合成文本
* @param sessionId 会话ID可传入32个字节的uuid或传入空内容由SDK自动生成。
* @param logLevel log打印级别值越小打印越多。
* @param saveLog 是否保存log为文件存储目录为ticket中的debug_path字段值。
* @return参见错误码https://help.aliyun.com/document_detail/459864.html。
*/
- (int) playStreamInputTts:(const char *)ticket parameters:(const char *)parameters
text:(const char*)text sessionId:(const char *)sessionId
logLevel:(NuiSdkLogLevel)logLevel saveLog:(BOOL)saveLog;
/**
* 与服务端完成建链,并开始单向流式语音合成(长文本语音合成)任务,异步接口
* @param ticket json string形式的鉴权参数参见下方说明或接口说明xxxxx
* @param parameters json string形式的初始化配置参数参见下方说明或接口说明xxxxx
* @param text 待合成文本
* @param sessionId 会话ID可传入32个字节的uuid或传入空内容由SDK自动生成。
* @param logLevel log打印级别值越小打印越多。
* @param saveLog 是否保存log为文件存储目录为ticket中的debug_path字段值。
* @return参见错误码https://help.aliyun.com/document_detail/459864.html。
*/
- (int) asyncPlayStreamInputTts:(const char *)ticket parameters:(const char *)parameters
text:(const char*)text sessionId:(const char *)sessionId
logLevel:(NuiSdkLogLevel)logLevel saveLog:(BOOL)saveLog;
/**
* 以流式的方式发送文本
* @param text 从大模型当中生成的流式文本
* @return参见错误码:https://help.aliyun.com/document_detail/459864.html。
*/
- (int) sendStreamInputTts:(const char *)text;
/**
* 结束合成任务通知服务端流入文本数据发送完毕阻塞等待服务端处理完成并返回所有合成音频。阻塞超时可以通过start接口中的complete_waiting_ms设置
* @return参见错误码:https://help.aliyun.com/document_detail/459864.html。
*/
- (int) stopStreamInputTts;
/**
* 结束合成任务,通知服务端流入文本数据发送完毕,不等待而是立即返回,同时回调继续返回剩余内容
* @return参见错误码:https://help.aliyun.com/document_detail/459864.html。
*/
- (int) asyncStopStreamInputTts;
/**
* 立即停止合成任务,不会有任何回调返回
* @return参见错误码:https://help.aliyun.com/document_detail/459864.html。
*/
- (int) cancelStreamInputTts;
/**
* 手动释放合成任务
*/
- (int) releaseStreamInputTts;
@end
#endif /* NeoNuiStreamInputTts_h */

Binary file not shown.

View File

@ -0,0 +1,88 @@
{
"cei":{
"cei_param_version":"2.2.0",
"cei_param_device_type":"car",
"cei_param_reco_mode":0,
"cei_param_log_level":2,
"cei_param_debug_path":"asr_debug",
"cei_param_is_debug_enable":false,
"cei_param_resource_path":"",
"cei_param_resource_is_multi_language":false,
"cei_param_audio_format_str":"16k16bitmono",
"cei_param_mcs_mode":0,
"cei_param_work_mode":0,
"cei_param_max_cache_frames":1000,
"cei_param_is_aec_bf_active":false,
"cei_param_is_agc_active":false,
"cei_param_is_vad_active":true,
"cei_param_is_kws_active":true,
"cei_param_is_sr_active":true
},
"asp":{
"asp_param_is_process_parallel":false,
"asp_param_is_input_debug_enable":false,
"asp_param_is_output_debug_enable":false,
"asp_param_debug_path":"asr_debug",
"asp_param_is_callback_enable":false,
"asp_param_callback_period_frames":5
},
"vad":{
"vad_param_is_input_debug_enable":false,
"vad_param_is_output_debug_enable":false,
"vad_param_debug_path":"asr_debug",
"vad_param_asleep_speech_noise_thres":-0.8,
"vad_param_awake_speech_noise_thres":-0.5,
"vad_param_asleep_max_speech_segment_time":300000,
"vad_param_awake_max_speech_segment_time":60000,
"vad_param_asleep_block_size":3,
"vad_param_awake_block_size":3,
"vad_param_front_timeout_interval":8000,
"vad_param_tail_timeout_interval":800,
"vad_param_is_detect_start":true,
"vad_param_is_detect_end":true
},
"kws":{
"kws_param_is_input_debug_enable":false,
"kws_param_is_output_debug_enable":false,
"kws_param_debug_path":"asr_debug",
"kws_param_is_process_parallel":false,
"kws_param_front_extend_frames":10,
"kws_param_tail_extend_frames":5,
"kws_param_encoder_type_str":"opu",
"kws_param_encoder_bitrate":16000,
"kws_param_encoder_complexity":2,
"kws_param_callback_period_ms":100,
"kws_param_max_frames_per_callback":25,
"kws_param_max_bytes_per_callback":16000
},
"sr":{
"sr_param_is_input_debug_enable":false,
"sr_param_is_output_debug_enable":false,
"sr_param_debug_path":"asr_debug",
"sr_param_is_itn_enable":true,
"sr_param_is_do_conf_filter":false,
"sr_param_is_process_parallel":true,
"sr_param_is_need_result":false,
"sr_param_is_need_voice":true,
"sr_param_ngram_conf_thres":65.0,
"sr_param_jsgf_conf_thres":65.0,
"sr_param_encoder_type_str":"opu",
"sr_param_encoder_bitrate":16000,
"sr_param_encoder_complexity":2,
"sr_param_callback_period_ms":100,
"sr_param_max_frames_per_callback":25,
"sr_param_max_bytes_per_callback":16000
},
"ou":{
"oss_upload_param_is_enable":false,
"oss_upload_param_asp_in":false,
"oss_upload_param_asp_out":false,
"oss_upload_param_vad_in":false,
"oss_upload_param_vad_out":false,
"oss_upload_param_kws_in":false,
"oss_upload_param_kws_susp_in":false,
"oss_upload_param_kws_out":false,
"oss_upload_param_kws_susp_out":false,
"oss_upload_param_sr_in":false
}
}

View File

@ -0,0 +1,3 @@
tts
nui.json
cei.json

View File

@ -0,0 +1,54 @@
{
"device_type":"phone",
"assets_version":"1.1.20241216",
"nui_config":{
"service_mode":"kModeFullCloud",
"log_level":"kLogLevelVerbose",
"enable_recorder_by_user":true,
"enable_dialog":false
},
"nls_config":{
"debug_level":4,
"sr_format":"opus",
"sample_rate":16000,
"dns_timeout":5000,
"vocab_default_weight":2
},
"audio_config":{
"debug":"none",
"16k_audio":{
"name":"16kmono16bit",
"id":0,
"mic":{
"name":"16kmono16bit",
"debug_heap_pollution":false,
"read_cnt":0,
"sample_rate":16000,
"bits_per_sample":16,
"channels":1,
"recording_interval":10,
"cei_frame_time_len":20,
"channel_mask":"kAMChannalMaskNone",
"format_type":"kAMDataFormatPcmInterleaved",
"endianness":"kAMByteOrderLittleEndian"
}
},
"8k_audio":{
"name":"8kmono16bit",
"id":1,
"mic":{
"name":"8kmono16bit",
"debug_heap_pollution":false,
"read_cnt":0,
"sample_rate":8000,
"bits_per_sample":16,
"channels":1,
"recording_interval":10,
"cei_frame_time_len":20,
"channel_mask":"kAMChannalMaskNone",
"format_type":"kAMDataFormatPcmInterleaved",
"endianness":"kAMByteOrderLittleEndian"
}
}
}
}

View File

@ -0,0 +1,31 @@
{
"device_type":"mobile",
"mode_type":2,
"smart_engine":3,
"encode_type":"pcm",
"debug_info":{
"debug_level":0,
"debug_path":"/sdcard/tmp"
},
"common_info":{
"pitch_level":0,
"speed_level":1.0,
"volume":1.0,
"play_audio":0,
"callback_raw_data":0,
"sample_rate":16000,
"font_name":"xiaoyun",
"wait_time":2000
},
"cache":{
"on":false,
"max_cache_size":2000000,
"save_path":"/sdcard/mit/"
},
"font":{
"on":false,
"mode":"cmd",
"save_path":"/sdcard/mit"
}
}

Binary file not shown.

BIN
Vendor/AliyunNui/nuisdk.framework/nuisdk vendored Executable file

Binary file not shown.

View File

@ -0,0 +1,358 @@
V2.6.4-028-20250410_iOS
NUI:
分支: release/2.6.x/baseline
commit id: 7427b38b4fd9723b267c7e5ba767405e7fbe646e
说明:
1. 减少识别时调用stop接口返回最终识别结果的延迟
2. 纯云端功能支持不传入workspace
3. 一句话识别增加链接超时相关参数设置;一句话识别新增EVENT_ASR_STARTED事件, 用于返回RecognitionStarted事件及task_id
4. 日志系统增加行号显示
5. 修复一句话识别返回2个EVENT_VAD_END的问题;修复一句话识别不返回错误信息和完整response的问题;
6. 修复录音文件极速版初始化失败时调用start崩溃的问题
7. 修复唤醒029在初始化时鉴权2次的问题; 纯离线功能时跳过token刷新功能
8. 允许非离线语音合成的SDK_CODE使用离线语音合成功能。但是不允许标准版离线语音合成使用精品版离线语音合成的发音人包
9. 录音文件识别极速版增加音频文件链接识别的功能
10.实时识别新增EVENT_TRANSCRIBER_STARTED事件, 用于返回TranscriptionStarted事件及task_id
11.修复离线TTS的SpeedRate&PitchRate&Volume设置无效的问题;修复离线TTS设置pitch_level后合成音频异常的问题;修复离线TTS设置错误extend_font_name会崩溃的问题
12.新增开关参数stash_partial_result。在实时识别和听悟实时推流时说长句子时SDK内部会将返回SentenceEnd的json repsone中的stash_result(暂存的中间结果)覆盖payload.result并以EVENT_ASR_PARTIAL_RESULT事件返回。新增参数将可开关这个功 >能
13.修复语音合成某些情况下get_param("error_msg")获得空结果的问题。
14.语音合成增加dns_timeout设置参数
15.日志系统增加设置控制日志文件大小的功能
16.流式语音合成增加cancel()和async_stop()接口
17.修复SDK使用新版本鉴权后退回到旧SDK使用旧版本鉴权启动会有json::LogicError-asCString崩溃的问题
18.增加日志信息回送的方式, 可通过此回调将SDK运行日志送到用户层
19.修复SSL获得数据时偶现TRY_AGAIN上报错误的问题
20.修复iOS平台使用时, 当获得的sockFd大于1024(FD_SETSIZE)时, connect()建连会失败, 上报EINPROGRESS错误和Invalid argument错误信息。
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: 55d88b7c81f28fb10720983b3840f6ee673d1798
说明:
1. 修复离线TTS的SpeedRate&PitchRate&Volume设置范围, 增加PitchRate设置接口
NuiDemoForUnit:
分支: develop/V2.6.x/baseline
commit id: 4d914b2c489d79ff3a577f46ad8c59feb40b84e0
说明:
1. Android/iOS 增加运行过程中刷新token的示例
2. Android/iOS 纯云端功能可不传入workspace
3. Android/iOS 一句话识别和实时识别增加启动事件EVENT_ASR_STARTED和EVENT_TRANSCRIBER_STARTED, 主要用于获得TaskId
4. Android 去掉不必要的权限android.permission.WRITE_EXTERNAL_STORAGE和android.permission.READ_EXTERNAL_STORAGE
5. Android 离线语音合成示例增加发音人语音包选择下载的功能增加SpeedRate和PitchRate和Volume设置方便演示
6. Android 语音合成示例增加SpeedRate和PitchRate和Volume设置增加音频保存的示例增加dns_timeout参数设置
7. Android 录音文件转写增加参数设置, 包括添加一些演示音频
8. Android 听悟实时推流demo新增关闭stash_partial_result的示例
9. Android/iOS 流式语音合成增加async_stop和cancel接口
10.Android 用于存储调试信息的debug_path调整到debug目录下。同时增加本地存储日志文件的最大字节数的设置。
11.Android 新增在线和离线语音合成混合使用的示例工程
12.iOS 去掉一些涉及权限信息的不必要操作
13.iOS 识别的调用接口改为同步,解决多实例运行可能崩溃的问题
V2.6.3-028-20241220_iOS
NUI:
分支: develop/2.6.3/baseline
commit id: 65097ea96579064487f2f51c9fefb7151858909d
说明:
1. 修复一句话识别返回2个EVENT_VAD_END的问题
2. 修复一句话识别不返回错误信息和完整response的问题
3. 修复录音文件极速版初始化失败时调用start崩溃的问题
4. 修复唤醒029在初始化时鉴权2次的问题; 纯离线功能时跳过token刷新功能
5. 允许非离线语音合成的SDK_CODE使用离线语音合成功能。但是不允许标准版离线语音合成使用精品版离线语音合成的发音人>包
6. 录音文件识别极速版增加音频文件链接识别的功能
7. 修复不传入workspace无法使用8K语音识别的问题
8. 实时识别新增EVENT_TRANSCRIBER_STARTED事件, 用于返回TranscriptionStarted事件及task_id
9. 一句话识别新增EVENT_ASR_STARTED事件, 用于返回RecognitionStarted事件及task_id
10.修复离线TTS的SpeedRate&PitchRate&Volume设置无效的问题
11.修复离线TTS设置错误extend_font_name会崩溃的问题
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: d2cb1d2245b2497fffb87f09b700b0468af61969
说明:
1. 修复离线TTS的SpeedRate&PitchRate&Volume设置范围, 增加PitchRate设置接口
NuiDemoForUnit:
分支: develop/V2.6.x/baseline
commit id: 193e89bc3d143b2ccef11cf262dd6a1154ad9e2c
说明:
1. iOS 增加运行过程中刷新token的示例
2. iOS 增加识别结果的展示
3. iOS device_id和workspace可不设置的说明
4. iOS 去掉一些涉及权限信息的操作
5. Android 移除android.permission.WRITE_EXTERNAL_STORAGE和android.permission.READ_EXTERNAL_STORAGE
6. Android 离线TTS demo增加发音人文件下载, 方便演示
7. Android TTS中增加语速语调音量发音人和音频编码的设置, 增加音频保存
8. Android 一句话识别和实时识别增加启动事件EVENT_ASR_STARTED和EVENT_TRANSCRIBER_STARTED, 主要用于获得TaskId
9. Android 录音文件转写增加参数设置, 包括添加一些演示音频
V2.6.0-028-20240708_iOS
NUI:
分支: develop/2.6.0/baseline
commit id: 3fd4f2b70939cb5045304511ac85f2b4f894615c
说明:
1.
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: acca4c2bad4a683aca444639fc16c55c01134dea
说明:
1.
NuiDemoForUnit:
分支: develop/V2.6.x/baseline
commit id: 5dcb206d05dd1051b4bcaa67dccfba42ef5fd598
说明:
1. 解决LOG_LEVEL_XXX命名冲突的问题
2. 解决离线TTS初始化传入空字符串崩溃的问题
V2.6.0-028-20240627_iOS
NUI:
分支: develop/2.6.0/baseline
commit id: 3fd4f2b70939cb5045304511ac85f2b4f894615c
说明:
1. OpenSSL增加加密套件协议支持绝大多数服务端握手
2. SDK打包中增加头文件可配合动态库进行cpp层面的调用开发
3. 流式TTS中参数设置不再仅支持字符串格式支持格式可见官网文档
4. 增加流式输入TTS
5. 增加单独唤醒的功能(仅029版本支持, 028/01B不支持)
6. Android增加带符号表的备份动态库libneonui_with_symbols用于崩溃时定位问题
7. 增加听悟实时记录功能
8. 回调返回完整信息
9. 改为OpenSSL作为底层网络库(仅01B支持)
10.解决AuthV3查看linux_version时fgets栈溢出的问题。
11.修复长文本语音合成文本超16K字节无法合成的问题。
12.修复android_api < 24 找不到fseeko64的问题。
13.在kws.bin vad.bin加载处增加try-catch包裹, 防止崩溃。
14.RelWithDebugInfo 有符号表, 可用这个成果物进行调试
15.在线TTS增加用户自定义参数设置接口的示例, 可以设置文档中不存在的参数
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: acca4c2bad4a683aca444639fc16c55c01134dea
说明:
1. 唤醒相关更新
2. 修复本地vad模块内存泄露问题(仅029版本支持, 028/01B不支持)
3. 本地唤醒模块升级(仅029版本支持, 028/01B不支持)
4. 在kws.bin vad.bin加载处增加try-catch包裹, 防止崩溃。
5. 增加对资源文件tts/languagedata_embedded.bin完整性校验仍然支持老的languagedata_embedded.bin
6. 打印信息中打印md5用于当kws资源文件异常时排查
NuiDemoForUnit:
分支: develop/V2.6.x/baseline
commit id: e3dfbec03d8ab510ba97fe39e83dd55258080d8d
说明:
1. 更新iOS示例工程中录音播放相关代码原旧版本录音播放相关代码继续保留
2. iOS/Android中鉴权相关代码重构支持SecurityToken的鉴权示例
3. Android中增加万能参数设置的示例
4. 增加流式输入TTS范例工程
5. 更新录音文件转写极速版范例工程
6. 增加单唤醒功能范例工程(仅029版本支持, 028/01B不支持)
7. 增加听悟实时记录范例工程
8. Android targetSdkVersion提升到33
V2.5.14-028-20230807_iOS
NUI:
分支: develop/2.5.15/baseline
commit id: 7be5435c4f2d881b406796fce824e9f891cde17d
说明:
1. 修复zlib 1.2.13引入的小于ANDROID_API<24时找不到fseeko64的问题
2. 修复超过16KB字节超长文本无法工作的问题引入多包功能若无法工作请切换深圳的url
3. 删除无用的代码
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: f5f0039f2f3a09d69445dd7530bb7c42511250da
说明:
1.
NuiDemoForUnit:
分支: develop/V2.5.15/baseline
commit id: 3f656fdc96c5bcfc369a1346863134c3a195079f
说明:
1. iOS工程中解决播放后快速暂停恢复无法播放音频的问题
resources:
见当前目录中resources
1. 离线TTS资源文件 tts/languagedata_embedded.bin 在原文件(md5:e8167925c9a9bc03272746950ba6ab5e)基础上增加md5头用于校验数据完整性。
2. 离线TTS资源文件 tts/languagedata_embedded.bin 添加补全SSML规则
Note:
V2.5.14-028-20230725_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: 2273440efac799361b29dfee5ee3706e54ceeff6
说明:
1. iOS 最低支持iOS9
2. iOS Clang 提供了一种控制退出时析构函数注册的机制, 增加-fno-c++-static-destructors编译选项解决偶现释放崩溃的问题。
3. 解决网络传输结束时偶现的多次释放线程导致的崩溃问题
4. 替换新的鉴权方案,兼容旧鉴权方案,隐私政策请详见《隐私政策.txt》
5. 升级zlib 1.2.11到1.2.13修复zlib的漏洞编号MPS-2022-53300
6. 修复不设置debug_path且workspace只读的情况下报错误码240022的问题。
7. 修复sts_token设置无效的问题
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: f5f0039f2f3a09d69445dd7530bb7c42511250da
说明:
1. 增加对资源文件tts/languagedata_embedded.bin完整性校验仍然支持老的languagedata_embedded.bin
2. 增加代码保护以解决离线TTS偶现的Tacotron::Tacotron(PhoneSet*) 崩溃问题。
3. iOS Clang 提供了一种控制退出时析构函数注册的机制, 增加-fno-c++-static-destructors编译选项解决偶现释放崩溃的问题。
4. iOS 支持最小版本为iOS9
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: edf69a4fdd19b1bdc37c3dc21318aac8327ec722
说明:
1. iOS nuisdk工程中CLANG_ENABLE_CPP_STATIC_DESTRUCTORS = NO 以解决偶现释放崩溃的问题
2. iOS demo工程中NLSRingBuffer.m中加入内存扩大机制解决TTS播放取数据太慢导致SDK内部数据塞爆std::vector崩溃的问题。
3. iOS demo工程在线TTS范例添加多段短文本语音合成的范例。
4. iOS demo工程离线TTS范例解决播放无声音的问题。
3. iOS&Android nuisdk工程增加获取手机硬件信息的功能用于离线TTS新鉴权方案。替换新的鉴权方案只获得符合安全和隐私规范的硬件信息。
4. iOS&Android demo工程更新阿里云账号范例强烈推荐使用sts安全方式获得账号。
5. iOS&Android demo工程获得用户权限进行收敛仅需要录音/播放权限、磁盘存储权限。
6. iOS&Android demo工程修复一句话识别示例中VAD/P2T模式设置的说明。
7. iOS&Android demo工程修复极速文件转写功能启动会进行鉴权的问题。
resources:
见当前目录中resources
1. 离线TTS资源文件 tts/languagedata_embedded.bin 在原文件(md5:061931cea06b913cfb4a7d1c0fbd686cMindJB)基础上增加md5头用于校验数据完整性
Note:
新鉴权方案(authV3)仅采集用户手机型号、主板型号等符合安全规范的信息。
V2.5.14-028-20230415_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: 243debb6bf25e44c775587435ebb258c79023c59
说明:
1. iOS: Clang 提供了一种控制退出时析构函数注册的机制, 可以使用>标志 -fno-c++-static-destructors全局禁用它们.
2. MBEDTLS_SSL_VERIFY_REQUIRED 改成 MBEDTLS_SSL_VERIFY_NONE. 解决服务器SSL证书中间证书颁发机构变化导致mbedtls握手失败. 这个是紧>急修复要求用户必须更新此版本以后SDK.
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: 446281f4517651d2dbfa84cc05e14f401ebda6b3
说明:
1. 修复英文唤醒词加载问题
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: 6e7386ba7b8d8facd5693746a3804ef95349c56b
说明:
1. xcode修改 CLANG_ENABLE_CPP_STATIC_DESTRUCTORS = NO
2. iOS demo工程的NLSRingBuf增加动态扩大机制防止超长文本语音合
成数据阻塞的问题
resources:
见当前目录中resources
Note:
V2.5.14-028-20230331_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: fb5c6c45984d7875aff52c9268418be5c32c9262
说明:
1. MBEDTLS_SSL_VERIFY_REQUIRED 改成 MBEDTLS_SSL_VERIFY_NONE
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: 1ee0641a3abc60039bf57041fd90b95f7bff8c31
说明:
1.
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: b4f3836c253b9d6525c8d1981e9a479c29d4ead9
说明:
1. iOS demo工程的NLSRingBuf增加动态扩大机制防止超长文本语音合成数据阻塞的问题
resources:
见当前目录中resources
Note:
V2.5.14-028-20221228_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: 58e6d066772077f487f45012ca1c2ac73c9e40b0
说明:
1. 增加STS支持
2. 修复动态唤醒词被设置默认参数的问题
3. 修复动态唤醒词空导致崩溃的问题
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: a736171ecd5f13f64d7501a2a3d5449b4b60da4b
说明:
1. 修复动态唤醒词空导致崩溃的问题
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: 9ec25aa1d25f61d11e8616494bae9d85fb60398b
说明:
1. demo中增加STS支持
resources:
见当前目录中resources
Note:
V2.5.14-028-20221208_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: cc7b123a423eff68b65a6f4997dd26f1ab3b73a8
说明:
1. 增加对长文本语音合成的支持
2. 为支持xcode14, 去除armv7&i386, 关闭BITCODE
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: 296534a988302322332e0fb82fb6374a9accc8ab
说明:
1. 为支持xcode14, 去除armv7&i386, 关闭BITCODE
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: 1db94805b92ea9ef3a8142950d89b173e839b8eb
说明:
1. 增加对长文本语音合成的支持
2. 为支持xcode14, 关闭BITCODE, ios最低11.0
resources:
见当前目录中resources
Note:
V2.5.14-028-20221108_iOS
NUI:
分支: develop/2.5.14-01B/unit_merge_taobaozhibo
commit id: 28950edab8ec3a3d0579a1aab05b752bb95b05ef
说明: 1. 修复NUI中speech_rate和speed_level换算错误的问题
SE(submodule):
分支: CEI_V2.3.x_Fix
commit id: f0123edf3320532c5a1b1f0abd72e70f3eac5425
说明: 1. 删除不用的LOGE
NuiDemoForUnit:
分支: release/V2.5.14-01B/unit_merge_taobaozhi
commit id: c47d8dec151e5a01f7c4e03054b807c5ead18c9c
说明:
1.
resources:
见当前目录中resources
Note:

View File

@ -62,6 +62,11 @@
path = suixinkanTests; path = suixinkanTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9A01B8E22FFB84950033C402 /* Vendor */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Vendor;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -115,6 +120,7 @@
children = ( children = (
93FF17C72FFB84950033C304 /* suixinkan */, 93FF17C72FFB84950033C304 /* suixinkan */,
A1B2C3D42FFB84950033C305 /* suixinkanTests */, A1B2C3D42FFB84950033C305 /* suixinkanTests */,
9A01B8E22FFB84950033C402 /* Vendor */,
93FF17C62FFB84950033C304 /* Products */, 93FF17C62FFB84950033C304 /* Products */,
365F718ADCDEA1FB75E2A740 /* Pods */, 365F718ADCDEA1FB75E2A740 /* Pods */,
27017AD20F47EB5E23B21608 /* Frameworks */, 27017AD20F47EB5E23B21608 /* Frameworks */,
@ -141,6 +147,7 @@
93FF17C12FFB84950033C304 /* Sources */, 93FF17C12FFB84950033C304 /* Sources */,
93FF17C22FFB84950033C304 /* Frameworks */, 93FF17C22FFB84950033C304 /* Frameworks */,
93FF17C32FFB84950033C304 /* Resources */, 93FF17C32FFB84950033C304 /* Resources */,
9A01B8E12FFB84950033C401 /* Embed Aliyun NUI SDK */,
D83AC0A37C1CDC54D21AB6C9 /* [CP] Copy Pods Resources */, D83AC0A37C1CDC54D21AB6C9 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
@ -315,6 +322,27 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9A01B8E12FFB84950033C401 /* Embed Aliyun NUI SDK */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/Vendor/AliyunNui/nuisdk.framework",
);
name = "Embed Aliyun NUI SDK";
outputFileListPaths = (
);
outputPaths = (
"$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/nuisdk.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\nif [ \"${PLATFORM_NAME}\" = \"iphonesimulator\" ]; then\n exit 0\nfi\nFRAMEWORK_SOURCE=\"${SRCROOT}/Vendor/AliyunNui/nuisdk.framework\"\nFRAMEWORK_DEST=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"${FRAMEWORK_DEST}\"\nrsync -a --delete \"${FRAMEWORK_SOURCE}\" \"${FRAMEWORK_DEST}/\"\nif [ -n \"${EXPANDED_CODE_SIGN_IDENTITY}\" ]; then\n /usr/bin/codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --preserve-metadata=identifier,entitlements,flags \"${FRAMEWORK_DEST}/nuisdk.framework\"\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -354,6 +382,10 @@
DEVELOPMENT_TEAM = 56GVN5RNVN; DEVELOPMENT_TEAM = 56GVN5RNVN;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(SRCROOT)/Vendor/AliyunNui",
);
INFOPLIST_FILE = suixinkan/Info.plist; INFOPLIST_FILE = suixinkan/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@ -369,6 +401,17 @@
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
); );
"OTHER_LDFLAGS[sdk=iphoneos*]" = (
"$(inherited)",
"-ObjC",
"-framework",
nuisdk,
"-framework",
AdSupport,
"-framework",
AudioToolbox,
"-liconv",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan; PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -397,6 +440,10 @@
DEVELOPMENT_TEAM = 56GVN5RNVN; DEVELOPMENT_TEAM = 56GVN5RNVN;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(SRCROOT)/Vendor/AliyunNui",
);
INFOPLIST_FILE = suixinkan/Info.plist; INFOPLIST_FILE = suixinkan/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@ -412,6 +459,17 @@
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
); );
"OTHER_LDFLAGS[sdk=iphoneos*]" = (
"$(inherited)",
"-ObjC",
"-framework",
nuisdk,
"-framework",
AdSupport,
"-framework",
AudioToolbox,
"-liconv",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan; PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;

View File

@ -0,0 +1,135 @@
//
// AliyunNLSTokenProvider.swift
// suixinkan
//
import CryptoKit
import Foundation
/// NLS token 访
struct AliyunNLSToken: Equatable {
let id: String
let expireTime: Int64?
}
/// NLS token 便 TTS
protocol AliyunNLSTokenProviding {
/// 使 STS NLS token
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken
}
/// NLS token Provider OpenAPI POP CreateToken
final class AliyunNLSTokenProvider: AliyunNLSTokenProviding {
typealias DataLoader = (URLRequest) async throws -> Data
private let endpoint: URL
private let dateProvider: () -> Date
private let nonceProvider: () -> String
private let dataLoader: DataLoader
init(
endpoint: URL = URL(string: "https://nls-meta.cn-shanghai.aliyuncs.com/")!,
dateProvider: @escaping () -> Date = Date.init,
nonceProvider: @escaping () -> String = { UUID().uuidString },
dataLoader: @escaping DataLoader = { request in
let (data, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
throw AliyunNLSTokenError.httpStatus(http.statusCode)
}
return data
}
) {
self.endpoint = endpoint
self.dateProvider = dateProvider
self.nonceProvider = nonceProvider
self.dataLoader = dataLoader
}
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken {
var params = signedBaseParams(credentials: credentials)
params["Signature"] = Self.signature(params: params, accessKeySecret: credentials.accessKeySecret)
let query = Self.canonicalQuery(params)
guard let url = URL(string: endpoint.absoluteString + "?" + query) else {
throw AliyunNLSTokenError.invalidURL
}
let request = URLRequest(url: url)
let data = try await dataLoader(request)
let response = try JSONDecoder().decode(AliyunNLSCreateTokenResponse.self, from: data)
guard let id = response.token?.id?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else {
throw AliyunNLSTokenError.missingToken
}
return AliyunNLSToken(id: id, expireTime: response.token?.expireTime)
}
/// CreateToken
func signedBaseParams(credentials: AliyunOSSCredentials) -> [String: String] {
[
"AccessKeyId": credentials.accessKeyId,
"Action": "CreateToken",
"Format": "JSON",
"RegionId": "cn-shanghai",
"SignatureMethod": "HMAC-SHA1",
"SignatureNonce": nonceProvider(),
"SignatureVersion": "1.0",
"Timestamp": Self.timestampFormatter.string(from: dateProvider()),
"Version": "2019-02-28",
"SecurityToken": credentials.securityToken,
]
}
/// POP HMAC-SHA1
static func signature(params: [String: String], accessKeySecret: String) -> String {
let canonical = canonicalQuery(params)
let stringToSign = "GET&%2F&\(percentEncode(canonical))"
let key = SymmetricKey(data: Data("\(accessKeySecret)&".utf8))
let mac = HMAC<Insecure.SHA1>.authenticationCode(for: Data(stringToSign.utf8), using: key)
return Data(mac).base64EncodedString()
}
/// OpenAPI
static func canonicalQuery(_ params: [String: String]) -> String {
params
.sorted { $0.key < $1.key }
.map { "\(percentEncode($0.key))=\(percentEncode($0.value))" }
.joined(separator: "&")
}
/// POP RFC3986
static func percentEncode(_ value: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-_.~")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}
private static let timestampFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
/// NLS token
enum AliyunNLSTokenError: Error, Equatable {
case invalidURL
case httpStatus(Int)
case missingToken
}
private struct AliyunNLSCreateTokenResponse: Decodable {
let token: TokenPayload?
enum CodingKeys: String, CodingKey {
case token = "Token"
}
struct TokenPayload: Decodable {
let id: String?
let expireTime: Int64?
enum CodingKeys: String, CodingKey {
case id = "Id"
case expireTime = "ExpireTime"
}
}
}

View File

@ -0,0 +1,59 @@
//
// AliyunNuiTTSBridge.h
// suixinkan
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 阿里云 NUI TTS 事件代理,将 Objective-C SDK 回调转成稳定的 Swift 边界。
@protocol AliyunNuiTTSBridgeDelegate;
/// 阿里云 NUI TTS Objective-C 桥接器,隔离 SDK 头文件和模拟器不可用逻辑。
@interface AliyunNuiTTSBridge : NSObject
/// SDK 回调代理。
@property (nonatomic, weak, nullable) id<AliyunNuiTTSBridgeDelegate> delegate;
/// 当前平台是否可直接加载 NUI TTS SDK。
@property (nonatomic, readonly) BOOL sdkAvailable;
/// 初始化 NUI TTS 引擎。
- (NSInteger)initializeWithTicket:(NSString *)ticket logLevel:(NSInteger)logLevel saveLog:(BOOL)saveLog;
/// 设置合成参数。
- (NSInteger)setParam:(NSString *)param value:(NSString *)value;
/// 开始合成指定文本。
- (NSInteger)playWithPriority:(NSString *)priority taskId:(NSString *)taskId text:(NSString *)text;
/// 取消指定任务taskId 为空时取消全部任务。
- (NSInteger)cancelWithTaskId:(nullable NSString *)taskId;
/// 释放 SDK 资源。
- (NSInteger)releaseEngine;
/// 获取 SDK 参数值。
- (nullable NSString *)paramValueForKey:(NSString *)key;
@end
/// 阿里云 NUI TTS 事件代理。
@protocol AliyunNuiTTSBridgeDelegate <NSObject>
/// 收到 TTS 状态事件。
- (void)aliyunNuiTTSBridge:(AliyunNuiTTSBridge *)bridge
didReceiveEvent:(NSInteger)event
taskId:(nullable NSString *)taskId
code:(NSInteger)code;
/// 收到 TTS 音频数据。
- (void)aliyunNuiTTSBridge:(AliyunNuiTTSBridge *)bridge
didReceiveAudioData:(NSData *)data
taskId:(nullable NSString *)taskId
info:(nullable NSString *)info;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,138 @@
//
// AliyunNuiTTSBridge.m
// suixinkan
//
#import "AliyunNuiTTSBridge.h"
#import <TargetConditionals.h>
#if !TARGET_OS_SIMULATOR && __has_include(<nuisdk/NeoNuiTts.h>)
#import <nuisdk/NeoNuiTts.h>
#define ALIYUN_NUI_TTS_AVAILABLE 1
#else
#define ALIYUN_NUI_TTS_AVAILABLE 0
#endif
#if ALIYUN_NUI_TTS_AVAILABLE
@interface AliyunNuiTTSBridge () <NeoNuiTtsDelegate>
@property (nonatomic, strong, nullable) NeoNuiTts *nui;
@end
#else
@interface AliyunNuiTTSBridge ()
@end
#endif
@implementation AliyunNuiTTSBridge
- (BOOL)sdkAvailable {
#if ALIYUN_NUI_TTS_AVAILABLE
return YES;
#else
return NO;
#endif
}
- (NSInteger)initializeWithTicket:(NSString *)ticket logLevel:(NSInteger)logLevel saveLog:(BOOL)saveLog {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui != nil) {
[self.nui nui_tts_release];
self.nui.delegate = nil;
self.nui = nil;
}
NeoNuiTts *nui = [NeoNuiTts get_instance];
nui.delegate = self;
NSInteger code = [nui nui_tts_initialize:[ticket UTF8String]
logLevel:(NuiSdkLogLevel)logLevel
saveLog:saveLog];
if (code == 0) {
self.nui = nui;
} else {
nui.delegate = nil;
[nui nui_tts_release];
}
return code;
#else
(void)ticket;
(void)logLevel;
(void)saveLog;
return -1;
#endif
}
- (NSInteger)setParam:(NSString *)param value:(NSString *)value {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui == nil) { return -1; }
return [self.nui nui_tts_set_param:[param UTF8String] value:[value UTF8String]];
#else
(void)param;
(void)value;
return -1;
#endif
}
- (NSInteger)playWithPriority:(NSString *)priority taskId:(NSString *)taskId text:(NSString *)text {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui == nil) { return -1; }
return [self.nui nui_tts_play:[priority UTF8String] taskId:[taskId UTF8String] text:[text UTF8String]];
#else
(void)priority;
(void)taskId;
(void)text;
return -1;
#endif
}
- (NSInteger)cancelWithTaskId:(NSString *)taskId {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui == nil) { return -1; }
const char *task = taskId.length > 0 ? [taskId UTF8String] : NULL;
return [self.nui nui_tts_cancel:task];
#else
(void)taskId;
return -1;
#endif
}
- (NSInteger)releaseEngine {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui == nil) { return 0; }
NSInteger code = [self.nui nui_tts_release];
self.nui.delegate = nil;
self.nui = nil;
return code;
#else
return 0;
#endif
}
- (NSString *)paramValueForKey:(NSString *)key {
#if ALIYUN_NUI_TTS_AVAILABLE
if (self.nui == nil) { return nil; }
const char *value = [self.nui nui_tts_get_param:[key UTF8String]];
if (value == NULL) { return nil; }
return [NSString stringWithUTF8String:value];
#else
(void)key;
return nil;
#endif
}
#if ALIYUN_NUI_TTS_AVAILABLE
- (void)onNuiTtsEventCallback:(NuiSdkTtsEvent)event taskId:(char *)taskid code:(int)code {
NSString *taskId = taskid != NULL ? [NSString stringWithUTF8String:taskid] : nil;
[self.delegate aliyunNuiTTSBridge:self didReceiveEvent:event taskId:taskId code:code];
}
- (void)onNuiTtsUserdataCallback:(char *)info infoLen:(int)info_len buffer:(char *)buffer len:(int)len taskId:(char *)task_id {
if (buffer == NULL || len <= 0) { return; }
NSData *data = [NSData dataWithBytes:buffer length:(NSUInteger)len];
NSString *taskId = task_id != NULL ? [NSString stringWithUTF8String:task_id] : nil;
NSString *infoText = nil;
if (info != NULL && info_len > 0) {
infoText = [[NSString alloc] initWithBytes:info length:(NSUInteger)info_len encoding:NSUTF8StringEncoding];
}
[self.delegate aliyunNuiTTSBridge:self didReceiveAudioData:data taskId:taskId info:infoText];
}
#endif
@end

View File

@ -0,0 +1,130 @@
//
// AliyunPCMStreamingPlayer.swift
// suixinkan
//
import AVFoundation
import Foundation
/// TTS PCM 便
protocol AliyunPCMStreamingPlaying: AnyObject {
/// 16-bit mono PCM
func start(sampleRate: Double)
/// 16-bit little-endian mono PCM
func write(_ data: Data)
///
func drain(_ completion: @escaping () -> Void)
///
func stop()
}
/// AVAudioEngine 16-bit PCM
final class AliyunPCMStreamingPlayer: AliyunPCMStreamingPlaying {
private let stateQueue = DispatchQueue(label: "com.zhifly.suixinkan.aliyunNLS.pcmPlayer")
private let engine = AVAudioEngine()
private let playerNode = AVAudioPlayerNode()
private var format: AVAudioFormat?
private var scheduledBufferCount = 0
private var draining = false
private var drainCompletion: (() -> Void)?
private var started = false
func start(sampleRate: Double) {
stateQueue.sync {
configureSession()
if engine.attachedNodes.contains(playerNode) == false {
engine.attach(playerNode)
}
let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: 1, interleaved: false)
format = audioFormat
if let audioFormat {
engine.disconnectNodeOutput(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat)
}
if engine.isRunning == false {
try? engine.start()
}
if playerNode.isPlaying == false {
playerNode.play()
}
started = true
draining = false
drainCompletion = nil
}
}
func write(_ data: Data) {
guard data.count >= 2 else { return }
stateQueue.async {
guard self.started, let format = self.format else { return }
let buffer = Self.makePCMBuffer(from: data, format: format)
guard let buffer else { return }
self.scheduledBufferCount += 1
self.playerNode.scheduleBuffer(buffer, completionCallbackType: .dataPlayedBack) { [weak self] _ in
self?.stateQueue.async {
guard let self else { return }
self.scheduledBufferCount = max(0, self.scheduledBufferCount - 1)
self.completeDrainIfNeeded()
}
}
}
}
func drain(_ completion: @escaping () -> Void) {
stateQueue.async {
self.draining = true
self.drainCompletion = completion
self.completeDrainIfNeeded()
}
}
func stop() {
stateQueue.sync {
playerNode.stop()
engine.stop()
scheduledBufferCount = 0
draining = false
drainCompletion = nil
started = false
}
}
private func completeDrainIfNeeded() {
guard draining, scheduledBufferCount == 0 else { return }
let completion = drainCompletion
drainCompletion = nil
draining = false
started = false
playerNode.stop()
completion?()
}
private func configureSession() {
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playback, options: [.allowBluetoothHFP, .mixWithOthers])
try? session.setActive(true)
}
private static func makePCMBuffer(from data: Data, format: AVAudioFormat) -> AVAudioPCMBuffer? {
let frameCount = data.count / MemoryLayout<Int16>.size
guard frameCount > 0,
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameCount)),
let channel = buffer.floatChannelData?[0] else {
return nil
}
buffer.frameLength = AVAudioFrameCount(frameCount)
data.withUnsafeBytes { raw in
let bytes = raw.bindMemory(to: UInt8.self)
for index in 0..<frameCount {
let low = UInt16(bytes[index * 2])
let high = UInt16(bytes[index * 2 + 1]) << 8
let sample = Int16(bitPattern: high | low)
channel[index] = Float(sample) / Float(Int16.max)
}
}
return buffer
}
}

View File

@ -11,8 +11,11 @@ protocol ScenicQueueTTSManaging: AnyObject {
/// ///
var customTextLooping: Bool { get } var customTextLooping: Bool { get }
/// 线
var supportsOfflineTTS: Bool { get }
/// TTS /// TTS
func prepareQueueTTSEngine(api: ScenicQueueAPI?) async func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async
/// ///
func startQueueCallLoop(queueNo: String) func startQueueCallLoop(queueNo: String)
@ -38,10 +41,7 @@ protocol ScenicQueueTTSManaging: AnyObject {
func estimateAnnouncementDurationMillis(_ text: String) -> Int64 func estimateAnnouncementDurationMillis(_ text: String) -> Int64
} }
/// NLS TTS /// NLS TTS 使 NUI SDK退
///
/// iOS NativeNui SDK STS/NLS SDK
/// SDK framework `prepareNativeEngineIfAvailable` NativeNui
final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynthesizerDelegate { final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynthesizerDelegate {
static let shared = AliyunNLSTTSManager() static let shared = AliyunNLSTTSManager()
@ -52,27 +52,50 @@ final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynth
private let synthesizer = AVSpeechSynthesizer() private let synthesizer = AVSpeechSynthesizer()
private let loopQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.tts") private let loopQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.tts")
private let nativeQueue = DispatchQueue(label: "com.zhifly.suixinkan.scenicQueue.aliyunNLS")
private let appStore: AppStore
private let tokenProvider: AliyunNLSTokenProviding
private let bridge: AliyunNuiTTSBridging
private let streamingPlayer: AliyunPCMStreamingPlaying
private var activeLoop: LoopKind? private var activeLoop: LoopKind?
private var currentContinuation: CheckedContinuation<Void, Never>? private var currentContinuation: CheckedContinuation<Void, Never>?
private var nlsToken: String? private var nativeContinuation: CheckedContinuation<Bool, Never>?
private var nlsToken: AliyunNLSToken?
private var lastCredentials: AliyunOSSCredentials? private var lastCredentials: AliyunOSSCredentials?
private var nativeReady = false
private(set) var customTextLooping = false private(set) var customTextLooping = false
let supportsOfflineTTS = false
override init() { init(
appStore: AppStore = .shared,
tokenProvider: AliyunNLSTokenProviding = AliyunNLSTokenProvider(),
bridge: AliyunNuiTTSBridging = AliyunNuiTTSBridgeAdapter(),
streamingPlayer: AliyunPCMStreamingPlaying = AliyunPCMStreamingPlayer()
) {
self.appStore = appStore
self.tokenProvider = tokenProvider
self.bridge = bridge
self.streamingPlayer = streamingPlayer
super.init() super.init()
synthesizer.delegate = self synthesizer.delegate = self
self.bridge.delegate = self
} }
func prepareQueueTTSEngine(api: ScenicQueueAPI?) async { func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
guard let api else { return } guard let api else { return }
guard !appStore.scenicQueueUseOfflineTTS || supportsOfflineTTS else {
nativeReady = false
return
}
do { do {
let sts = try await api.aliyunSTS(bucket: "vipsky") let sts = try await api.aliyunSTS(bucket: "vipsky")
lastCredentials = sts.credentials lastCredentials = sts.credentials
nlsToken = nil let token = try await tokenProvider.createToken(credentials: sts.credentials)
prepareNativeEngineIfAvailable(credentials: sts.credentials) nlsToken = token
nativeReady = await prepareNativeEngineIfAvailable(token: token)
} catch { } catch {
// Voice playback falls back to AVSpeechSynthesizer if NLS warm-up fails. nativeReady = false
} }
} }
@ -182,6 +205,35 @@ final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynth
} }
private func speak(_ text: String) async { private func speak(_ text: String) async {
if await speakWithNativeEngine(text) {
return
}
await speakWithSystemVoice(text)
}
private func speakWithNativeEngine(_ text: String) async -> Bool {
await withCheckedContinuation { continuation in
nativeQueue.async { [weak self] in
guard let self, self.nativeReady, self.bridge.sdkAvailable else {
continuation.resume(returning: false)
return
}
self.cancelNativeSpeakLocked(resumeExisting: true)
self.nativeContinuation = continuation
self.streamingPlayer.stop()
self.streamingPlayer.start(sampleRate: Self.nuiSampleRate)
self.applyNativePlaybackParams(text: text)
let code = self.bridge.play(priority: "1", taskId: Self.nuiTaskId, text: text)
if code != 0 {
self.streamingPlayer.stop()
self.nativeContinuation = nil
continuation.resume(returning: false)
}
}
}
}
private func speakWithSystemVoice(_ text: String) async {
await MainActor.run { await MainActor.run {
if synthesizer.isSpeaking { if synthesizer.isSpeaking {
synthesizer.stopSpeaking(at: .immediate) synthesizer.stopSpeaking(at: .immediate)
@ -200,6 +252,9 @@ final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynth
} }
private func stopSpeaking() { private func stopSpeaking() {
nativeQueue.async { [weak self] in
self?.cancelNativeSpeakLocked(resumeExisting: true)
}
DispatchQueue.main.async { DispatchQueue.main.async {
if self.synthesizer.isSpeaking { if self.synthesizer.isSpeaking {
self.synthesizer.stopSpeaking(at: .immediate) self.synthesizer.stopSpeaking(at: .immediate)
@ -221,8 +276,200 @@ final class AliyunNLSTTSManager: NSObject, ScenicQueueTTSManaging, AVSpeechSynth
resumeCurrentContinuationIfNeeded() resumeCurrentContinuationIfNeeded()
} }
private func prepareNativeEngineIfAvailable(credentials: AliyunOSSCredentials) { private func prepareNativeEngineIfAvailable(token: AliyunNLSToken) async -> Bool {
_ = NSClassFromString("NativeNui") guard bridge.sdkAvailable,
_ = credentials let ticket = nativeOnlineTicket(token: token.id) else {
return false
}
return await withCheckedContinuation { continuation in
nativeQueue.async { [weak self] in
guard let self else {
continuation.resume(returning: false)
return
}
self.cancelNativeSpeakLocked(resumeExisting: true)
_ = self.bridge.releaseEngine()
let code = self.bridge.initialize(ticket: ticket, logLevel: 0, saveLog: true)
guard code == 0 else {
continuation.resume(returning: false)
return
}
self.applyNativeQueueProsodyParams()
continuation.resume(returning: true)
}
}
}
private func nativeOnlineTicket(token: String) -> String? {
guard let workspace = nativeWorkspacePath() else { return nil }
let ticket: [String: String] = [
"app_key": Self.onlineAppKey,
"token": token,
"workspace": workspace,
"debug_path": nativeDebugPath(),
"url": "wss://nls-gateway.cn-shanghai.aliyuncs.com:443/ws/v1",
"device_id": Self.deviceId(),
"mode_type": "2",
"tts_version": "0",
]
guard let data = try? JSONSerialization.data(withJSONObject: ticket) else { return nil }
return String(data: data, encoding: .utf8)
}
private func nativeWorkspacePath() -> String? {
if let privateFrameworksPath = Bundle.main.privateFrameworksPath {
let frameworkPath = (privateFrameworksPath as NSString).appendingPathComponent("nuisdk.framework")
if let frameworkBundle = Bundle(path: frameworkPath),
let path = frameworkBundle.path(forResource: "Resources", ofType: "bundle") {
return path
}
}
return Bundle.main.path(forResource: "Resources", ofType: "bundle")
}
private func nativeDebugPath() -> String {
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
let url = (caches ?? URL(fileURLWithPath: NSTemporaryDirectory())).appendingPathComponent("aliyun_nls_tts_debug", isDirectory: true)
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url.path
}
private func applyNativePlaybackParams(text: String) {
_ = bridge.setParam("mode_type", value: "2")
_ = bridge.setParam("tts_version", value: text.count > 300 ? "1" : "0")
applyNativeQueueProsodyParams()
}
private func applyNativeQueueProsodyParams() {
_ = bridge.setParam("speech_rate", value: Self.onlineSpeechRate)
_ = bridge.setParam("volume", value: Self.playbackVolume)
}
private func cancelNativeSpeakLocked(resumeExisting: Bool) {
_ = bridge.cancel(taskId: Self.nuiTaskId)
streamingPlayer.stop()
if resumeExisting {
finishNativeSpeakLocked(success: false)
} else {
nativeContinuation = nil
}
}
private func finishNativeSpeakLocked(success: Bool) {
let continuation = nativeContinuation
nativeContinuation = nil
continuation?.resume(returning: success)
}
private static func deviceId() -> String {
let key = "aliyun_nls_tts_device_id_v1"
if let value = UserDefaults.standard.string(forKey: key), !value.isEmpty {
return value
}
let value = UUID().uuidString
UserDefaults.standard.set(value, forKey: key)
return value
}
private static let onlineAppKey = "3WX8cD04JCGkbRLi"
private static let nuiTaskId = "1"
private static let nuiSampleRate = 16_000.0
private static let onlineSpeechRate = "-120"
private static let playbackVolume = "2.0"
}
/// NUI TTS Swift
protocol AliyunNuiTTSBridging: AnyObject {
/// SDK
var delegate: AliyunNuiTTSBridgeDelegate? { get set }
/// 使 SDK
var sdkAvailable: Bool { get }
///
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int
///
func setParam(_ param: String, value: String) -> Int
///
func play(priority: String, taskId: String, text: String) -> Int
///
func cancel(taskId: String?) -> Int
///
func releaseEngine() -> Int
/// SDK
func paramValue(for key: String) -> String?
}
/// NUI TTS Objective-C Swift
final class AliyunNuiTTSBridgeAdapter: AliyunNuiTTSBridging {
private let bridge: AliyunNuiTTSBridge
var delegate: AliyunNuiTTSBridgeDelegate? {
get { bridge.delegate }
set { bridge.delegate = newValue }
}
var sdkAvailable: Bool { bridge.sdkAvailable }
init(bridge: AliyunNuiTTSBridge = AliyunNuiTTSBridge()) {
self.bridge = bridge
}
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
bridge.initialize(withTicket: ticket, logLevel: logLevel, saveLog: saveLog)
}
func setParam(_ param: String, value: String) -> Int {
bridge.setParam(param, value: value)
}
func play(priority: String, taskId: String, text: String) -> Int {
bridge.play(withPriority: priority, taskId: taskId, text: text)
}
func cancel(taskId: String?) -> Int {
bridge.cancel(withTaskId: taskId)
}
func releaseEngine() -> Int {
bridge.releaseEngine()
}
func paramValue(for key: String) -> String? {
bridge.paramValue(forKey: key)
}
}
extension AliyunNLSTTSManager: AliyunNuiTTSBridgeDelegate {
func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveEvent event: Int, taskId: String?, code: Int) {
nativeQueue.async { [weak self] in
guard let self else { return }
switch event {
case 1:
self.streamingPlayer.drain { [weak self] in
self?.nativeQueue.async {
self?.finishNativeSpeakLocked(success: true)
}
}
case 2, 5:
self.streamingPlayer.stop()
self.finishNativeSpeakLocked(success: false)
default:
break
}
_ = taskId
_ = code
}
}
func aliyunNuiTTSBridge(_ bridge: AliyunNuiTTSBridge, didReceiveAudioData data: Data, taskId: String?, info: String?) {
streamingPlayer.write(data)
_ = taskId
_ = info
} }
} }

View File

@ -103,6 +103,11 @@ final class ScenicQueueSettingsViewModel {
return "请选择打卡点" return "请选择打卡点"
} }
/// 使线
var useOfflineTTS: Bool {
appStore.scenicQueueUseOfflineTTS
}
func openSettingChangeLog() { func openSettingChangeLog() {
onNavigateLog?() onNavigateLog?()
} }
@ -170,6 +175,23 @@ final class ScenicQueueSettingsViewModel {
notifyStateChange() notifyStateChange()
} }
/// 线
func setUseOfflineTTS(_ enabled: Bool) {
guard enabled else {
appStore.scenicQueueUseOfflineTTS = false
notifyStateChange()
return
}
guard ttsManager.supportsOfflineTTS else {
appStore.scenicQueueUseOfflineTTS = false
onShowMessage?("离线语音资源未安装")
notifyStateChange()
return
}
appStore.scenicQueueUseOfflineTTS = true
notifyStateChange()
}
func setCustomTTSText(_ value: String) { func setCustomTTSText(_ value: String) {
customTTSText = String(value.prefix(Limits.remarkMaxLen)) customTTSText = String(value.prefix(Limits.remarkMaxLen))
notifyStateChange() notifyStateChange()

View File

@ -87,7 +87,7 @@ final class ScenicQueueViewModel {
await refreshQueueStats(api: api) await refreshQueueStats(api: api)
await refreshCurrentQueue(api: api, reset: true) await refreshCurrentQueue(api: api, reset: true)
await refreshSkippedQueue(api: api, reset: true) await refreshSkippedQueue(api: api, reset: true)
await ttsManager.prepareQueueTTSEngine(api: api as? ScenicQueueAPI) await ttsManager.prepareQueueTTSEngine(api: api)
} else { } else {
resetQueues() resetQueues()
onRequireSettings?() onRequireSettings?()

View File

@ -166,6 +166,7 @@ final class ScenicQueueSettingsViewController: BaseViewController {
private func buildVoiceForm() { private func buildVoiceForm() {
addTextRow(key: "broadcastInterval", title: "播报间隔时间", value: "\(viewModel.broadcastIntervalSec)", suffix: "", keyboard: .numberPad, action: #selector(broadcastChanged(_:))) addTextRow(key: "broadcastInterval", title: "播报间隔时间", value: "\(viewModel.broadcastIntervalSec)", suffix: "", keyboard: .numberPad, action: #selector(broadcastChanged(_:)))
addTextRow(key: "countdown", title: "进入读秒倒计时阈值", value: "\(viewModel.countdownThresholdSec)", suffix: "", keyboard: .numberPad, action: #selector(countdownChanged(_:))) addTextRow(key: "countdown", title: "进入读秒倒计时阈值", value: "\(viewModel.countdownThresholdSec)", suffix: "", keyboard: .numberPad, action: #selector(countdownChanged(_:)))
addSwitchRow(key: "offlineTTS", title: "离线语音合成", isOn: viewModel.useOfflineTTS, action: #selector(offlineTTSChanged(_:)))
stackView.addArrangedSubview(actionCard(title: "蓝牙音响测试", value: "请确认外放音量是否合适", primary: "播放测试音", secondary: nil, primaryAction: #selector(testSoundTapped), secondaryAction: nil)) stackView.addArrangedSubview(actionCard(title: "蓝牙音响测试", value: "请确认外放音量是否合适", primary: "播放测试音", secondary: nil, primaryAction: #selector(testSoundTapped), secondaryAction: nil))
addTextRow(key: "customTTS", title: "自定义播报文案", value: viewModel.customTTSText, suffix: "", keyboard: .default, action: #selector(customTTSChanged(_:))) addTextRow(key: "customTTS", title: "自定义播报文案", value: viewModel.customTTSText, suffix: "", keyboard: .default, action: #selector(customTTSChanged(_:)))
stackView.addArrangedSubview(actionCard(title: "播报文案", value: "最多保存5个语音", primary: "播放文本", secondary: "保存到预设", primaryAction: #selector(playCustomTapped), secondaryAction: #selector(savePresetTapped))) stackView.addArrangedSubview(actionCard(title: "播报文案", value: "最多保存5个语音", primary: "播放文本", secondary: "保存到预设", primaryAction: #selector(playCustomTapped), secondaryAction: #selector(savePresetTapped)))
@ -329,6 +330,7 @@ final class ScenicQueueSettingsViewController: BaseViewController {
@objc private func secondAheadChanged(_ sender: UITextField) { viewModel.setSecondAheadCount(Int(sender.text ?? "") ?? 0) } @objc private func secondAheadChanged(_ sender: UITextField) { viewModel.setSecondAheadCount(Int(sender.text ?? "") ?? 0) }
@objc private func broadcastChanged(_ sender: UITextField) { viewModel.setBroadcastIntervalSec(Int(sender.text ?? "") ?? 50) } @objc private func broadcastChanged(_ sender: UITextField) { viewModel.setBroadcastIntervalSec(Int(sender.text ?? "") ?? 50) }
@objc private func countdownChanged(_ sender: UITextField) { viewModel.setCountdownThresholdSec(Int(sender.text ?? "") ?? 15) } @objc private func countdownChanged(_ sender: UITextField) { viewModel.setCountdownThresholdSec(Int(sender.text ?? "") ?? 15) }
@objc private func offlineTTSChanged(_ sender: UISwitch) { viewModel.setUseOfflineTTS(sender.isOn) }
@objc private func customTTSChanged(_ sender: UITextField) { viewModel.setCustomTTSText(sender.text ?? "") } @objc private func customTTSChanged(_ sender: UITextField) { viewModel.setCustomTTSText(sender.text ?? "") }
} }

View File

@ -3,11 +3,26 @@
// suixinkan // suixinkan
// //
#if __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
#import <AMapFoundationKit/AMapFoundationKit.h> #import <AMapFoundationKit/AMapFoundationKit.h>
#endif
#if __has_include(<AMapLocationKit/AMapLocationKit.h>)
#import <AMapLocationKit/AMapLocationKit.h> #import <AMapLocationKit/AMapLocationKit.h>
#endif
#if __has_include(<MAMapKit/MAMapKit.h>)
#import <MAMapKit/MAMapKit.h> #import <MAMapKit/MAMapKit.h>
#endif
#if __has_include(<AMapSearchKit/AMapSearchKit.h>)
#import <AMapSearchKit/AMapSearchKit.h> #import <AMapSearchKit/AMapSearchKit.h>
#endif
#if __has_include(<UMCommon/UMCommon.h>)
#import <UMCommon/UMCommon.h> #import <UMCommon/UMCommon.h>
#endif
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
#if __has_include(<UMAPM/UMAPM.h>) #if __has_include(<UMAPM/UMAPM.h>)
#import <UMAPM/UMAPM.h> #import <UMAPM/UMAPM.h>

View File

@ -271,6 +271,68 @@ final class ScenicQueueFeatureTests: XCTestCase {
XCTAssertEqual(store.scenicQueuePunchSpotId, "") XCTAssertEqual(store.scenicQueuePunchSpotId, "")
XCTAssertNil(store.scenicQueueSettingsSnapshot()) XCTAssertNil(store.scenicQueueSettingsSnapshot())
} }
func testAliyunNLSTokenProviderBuildsSignedCreateTokenRequest() async throws {
let credentials = try MockScenicQueueAPI.makeSTS().credentials
let provider = AliyunNLSTokenProvider(
dateProvider: { Date(timeIntervalSince1970: 1_704_067_200) },
nonceProvider: { "nonce-1" },
dataLoader: { request in
let url = try XCTUnwrap(request.url)
let query = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems)
XCTAssertEqual(query.first(where: { $0.name == "Action" })?.value, "CreateToken")
XCTAssertEqual(query.first(where: { $0.name == "AccessKeyId" })?.value, "ak")
XCTAssertEqual(query.first(where: { $0.name == "SecurityToken" })?.value, "security-token")
XCTAssertEqual(query.first(where: { $0.name == "SignatureMethod" })?.value, "HMAC-SHA1")
XCTAssertNotNil(query.first(where: { $0.name == "Signature" })?.value)
return #"{"Token":{"Id":"nls-token","ExpireTime":1704068200}}"#.data(using: .utf8)!
}
)
let token = try await provider.createToken(credentials: credentials)
XCTAssertEqual(token.id, "nls-token")
XCTAssertEqual(token.expireTime, 1_704_068_200)
}
func testAliyunTTSManagerUsesNativeBridgeAndPlayerWhenPrepared() async {
let api = MockScenicQueueAPI()
let tokenProvider = MockAliyunNLSTokenProvider()
let bridge = MockAliyunNuiTTSBridge()
let player = MockAliyunPCMStreamingPlayer()
let manager = AliyunNLSTTSManager(
tokenProvider: tokenProvider,
bridge: bridge,
streamingPlayer: player
)
await manager.prepareQueueTTSEngine(api: api)
let ok = await manager.speakPlainTextOnce("A12号请到拍摄区域准备拍摄。")
XCTAssertTrue(ok)
XCTAssertTrue(bridge.events.contains { $0.hasPrefix("initialize:") })
XCTAssertTrue(bridge.events.contains("set:speech_rate=-120"))
XCTAssertTrue(bridge.events.contains("set:volume=2.0"))
XCTAssertTrue(bridge.events.contains("play:A12号请到拍摄区域准备拍摄。"))
XCTAssertEqual(Array(player.events.suffix(3)), ["start:16000", "write:4", "drain"])
}
func testSettingsOfflineTTSSwitchReportsUnavailable() {
let defaults = UserDefaults(suiteName: "ScenicQueueFeatureTests.offlineTTS")!
defaults.removePersistentDomain(forName: "ScenicQueueFeatureTests.offlineTTS")
let store = AppStore(defaults: defaults)
store.userId = "u1"
store.accountType = "photog"
let tts = MockScenicQueueTTSManager()
let viewModel = ScenicQueueSettingsViewModel(appStore: store, ttsManager: tts)
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
viewModel.setUseOfflineTTS(true)
XCTAssertFalse(store.scenicQueueUseOfflineTTS)
XCTAssertEqual(messages.last, "离线语音资源未安装")
}
} }
@MainActor @MainActor
@ -355,8 +417,9 @@ private final class MockScenicQueueAPI: ScenicQueueAPIProtocol {
private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging { private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging {
private(set) var events: [String] = [] private(set) var events: [String] = []
private(set) var customTextLooping = false private(set) var customTextLooping = false
let supportsOfflineTTS = false
func prepareQueueTTSEngine(api: ScenicQueueAPI?) async { func prepareQueueTTSEngine(api: ScenicQueueAPIProtocol?) async {
events.append("prepare") events.append("prepare")
} }
@ -393,6 +456,76 @@ private final class MockScenicQueueTTSManager: ScenicQueueTTSManaging {
} }
} }
private final class MockAliyunNLSTokenProvider: AliyunNLSTokenProviding {
func createToken(credentials: AliyunOSSCredentials) async throws -> AliyunNLSToken {
AliyunNLSToken(id: "mock-nls-token", expireTime: 1_704_068_200)
}
}
private final class MockAliyunNuiTTSBridge: AliyunNuiTTSBridging {
weak var delegate: AliyunNuiTTSBridgeDelegate?
var sdkAvailable = true
private(set) var events: [String] = []
private let callbackBridge = AliyunNuiTTSBridge()
func initialize(ticket: String, logLevel: Int, saveLog: Bool) -> Int {
events.append("initialize:\(ticket.contains("mock-nls-token"))")
return 0
}
func setParam(_ param: String, value: String) -> Int {
events.append("set:\(param)=\(value)")
return 0
}
func play(priority: String, taskId: String, text: String) -> Int {
events.append("play:\(text)")
delegate?.aliyunNuiTTSBridge(
callbackBridge,
didReceiveAudioData: Data([0, 0, 1, 0]),
taskId: taskId,
info: nil
)
delegate?.aliyunNuiTTSBridge(callbackBridge, didReceiveEvent: 1, taskId: taskId, code: 0)
return 0
}
func cancel(taskId: String?) -> Int {
events.append("cancel:\(taskId ?? "")")
return 0
}
func releaseEngine() -> Int {
events.append("release")
return 0
}
func paramValue(for key: String) -> String? {
nil
}
}
private final class MockAliyunPCMStreamingPlayer: AliyunPCMStreamingPlaying {
private(set) var events: [String] = []
func start(sampleRate: Double) {
events.append("start:\(Int(sampleRate))")
}
func write(_ data: Data) {
events.append("write:\(data.count)")
}
func drain(_ completion: @escaping () -> Void) {
events.append("drain")
completion()
}
func stop() {
events.append("stop")
}
}
private extension ScenicQueueHomeData { private extension ScenicQueueHomeData {
static func page(items: [ScenicQueueHomeItem], total: Int, page: Int) -> ScenicQueueHomeData { static func page(items: [ScenicQueueHomeItem], total: Int, page: Int) -> ScenicQueueHomeData {
ScenicQueueHomeData( ScenicQueueHomeData(