// // SenseVoiceBridge.mm // 康康 // // ObjC++ 实现。链接 sherpa-mnn 时走真实 C-API;否则编为桩(返回不可用,上层回退系统识别)。 // // 可用性闸门:仅当能找到 sherpa-mnn 的 C-API 头时才编真实路径。这样在尚未接入 sherpa-mnn // 的工程里本文件也能正常编过(走桩),接入后(把 sherpa-mnn.xcframework 加进 Frameworks/ 并 // 让 HEADER_SEARCH_PATHS 找到其头)自动切到真实实现。见 docs/release/sensevoice-integration.md。 // #import "SenseVoiceBridge.h" #if __has_include() #define KK_SHERPA_MNN_AVAILABLE 1 #else #define KK_SHERPA_MNN_AVAILABLE 0 #endif #if !KK_SHERPA_MNN_AVAILABLE // ============ 桩:工程未链接 sherpa-mnn ============ @implementation SenseVoiceBridge + (BOOL)isAvailable { return NO; } - (nullable instancetype)initWithModelPath:(NSString *)modelPath tokensPath:(NSString *)tokensPath language:(NSString *)language { return nil; } - (nullable NSString *)transcribeSamples:(const float *)samples count:(int)count sampleRate:(int)sampleRate { return nil; } @end #else // ============ 真实:sherpa-mnn 离线 SenseVoice ============ #include #include @implementation SenseVoiceBridge { const SherpaMnnOfflineRecognizer *_recognizer; } + (BOOL)isAvailable { return YES; } - (nullable instancetype)initWithModelPath:(NSString *)modelPath tokensPath:(NSString *)tokensPath language:(NSString *)language { self = [super init]; if (!self) return nil; std::string model = modelPath.UTF8String; std::string tokens = tokensPath.UTF8String; std::string lang = language.length ? language.UTF8String : "auto"; SherpaMnnOfflineSenseVoiceModelConfig senseVoice; memset(&senseVoice, 0, sizeof(senseVoice)); senseVoice.model = model.c_str(); senseVoice.language = lang.c_str(); senseVoice.use_itn = 1; // 逆文本规整:把「一百四十」之类还原成 140,数值更利于阅读/给医生看 SherpaMnnOfflineModelConfig modelConfig; memset(&modelConfig, 0, sizeof(modelConfig)); modelConfig.tokens = tokens.c_str(); modelConfig.num_threads = 2; // 端侧保守取 2,避免与 UI 抢核 modelConfig.debug = 0; modelConfig.provider = "cpu"; // sherpa-mnn 后端即 MNN(CPU/SME2),provider 维持 "cpu" modelConfig.sense_voice = senseVoice; SherpaMnnOfflineRecognizerConfig config; memset(&config, 0, sizeof(config)); config.decoding_method = "greedy_search"; config.model_config = modelConfig; _recognizer = SherpaMnnCreateOfflineRecognizer(&config); if (_recognizer == nullptr) return nil; return self; } - (void)dealloc { if (_recognizer) { SherpaMnnDestroyOfflineRecognizer(_recognizer); _recognizer = nullptr; } } - (nullable NSString *)transcribeSamples:(const float *)samples count:(int)count sampleRate:(int)sampleRate { if (_recognizer == nullptr || samples == nullptr || count <= 0) return nil; const SherpaMnnOfflineStream *stream = SherpaMnnCreateOfflineStream(_recognizer); if (stream == nullptr) return nil; SherpaMnnAcceptWaveformOffline(stream, sampleRate, samples, count); SherpaMnnDecodeOfflineStream(_recognizer, stream); NSString *text = nil; const SherpaMnnOfflineRecognizerResult *result = SherpaMnnGetOfflineStreamResult(stream); if (result) { if (result->text) { text = [NSString stringWithUTF8String:result->text]; } SherpaMnnDestroyOfflineRecognizerResult(result); } SherpaMnnDestroyOfflineStream(stream); return text; } @end #endif