react-native-davoice-tts 1.0.387 → 1.0.389
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TTSRNBridge.podspec +1 -1
- package/ios/SpeechBridge/SpeechBridge.m +252 -47
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/Info.plist +5 -5
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/DavoiceTTS +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.abi.json +325 -283
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/DavoiceTTS +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.abi.json +325 -283
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.abi.json +325 -283
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeRequirements-1 +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeResources +18 -18
- package/package.json +1 -1
- package/speech/index.ts +10 -0
package/TTSRNBridge.podspec
CHANGED
|
@@ -2,7 +2,7 @@ require 'json'
|
|
|
2
2
|
|
|
3
3
|
Pod::Spec.new do |s|
|
|
4
4
|
s.name = "TTSRNBridge"
|
|
5
|
-
s.version = "1.0.
|
|
5
|
+
s.version = "1.0.345" # Update to your package version
|
|
6
6
|
s.summary = "TTS for React Native."
|
|
7
7
|
s.description = <<-DESC
|
|
8
8
|
A React Native module for tts .
|
|
@@ -22,6 +22,22 @@ static void SBDebugLog(NSString *format, ...)
|
|
|
22
22
|
va_end(args);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Stable on-disk location for a downloaded http(s) asset, keyed by URL. Metro asset
|
|
26
|
+
// URLs carry a content hash, so the same URL means the same bytes and the file can be
|
|
27
|
+
// reused across calls and launches instead of re-downloading (e.g. on every wake-word
|
|
28
|
+
// ping sound in a debug build) every time.
|
|
29
|
+
static NSString *SBCachedFilePathForRemoteAsset(NSString *urlString) {
|
|
30
|
+
NSString *ext = [NSURL URLWithString:urlString].pathExtension;
|
|
31
|
+
if (ext.length == 0) ext = @"wav";
|
|
32
|
+
unsigned long long h = 1469598103934665603ULL; // FNV-1a: deterministic across launches
|
|
33
|
+
for (NSUInteger i = 0; i < urlString.length; i++) {
|
|
34
|
+
h ^= (unsigned long long)[urlString characterAtIndex:i];
|
|
35
|
+
h *= 1099511628211ULL;
|
|
36
|
+
}
|
|
37
|
+
NSString *name = [NSString stringWithFormat:@"rn_asset_%016llx.%@", h, ext];
|
|
38
|
+
return [NSTemporaryDirectory() stringByAppendingPathComponent:name];
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
static NSData *SB_Base64Decode(NSString *b64) {
|
|
26
42
|
if (!b64 || (id)b64 == [NSNull null]) return nil;
|
|
27
43
|
return [[NSData alloc] initWithBase64EncodedString:b64 options:0];
|
|
@@ -137,6 +153,34 @@ static AVAudioPCMBuffer *SB_MakeMonoF32Buffer(NSData *raw,
|
|
|
137
153
|
return buf;
|
|
138
154
|
}
|
|
139
155
|
|
|
156
|
+
// The STT/DaVoiceTTS core is main-queue affine: it schedules its own internal
|
|
157
|
+
// polling on the main queue and mutates AVAudioEngine/task state without a lock
|
|
158
|
+
// guarding every access, so every touch of self.stt from this bridge must also
|
|
159
|
+
// stay on main -- mixing threads here races that internal state. -methodQueue
|
|
160
|
+
// below keeps every RCT_EXPORT_METHOD call on main already; this guard exists
|
|
161
|
+
// so a future call site added off that path (a completion handler, a hop to a
|
|
162
|
+
// background queue, etc.) fails loudly instead of silently reintroducing that
|
|
163
|
+
// race. DEBUG asserts at the violating frame; release logs once via NSLog
|
|
164
|
+
// rather than RCTLogError, since RCTLogError is a no-op in a release build
|
|
165
|
+
// with no log function installed (RCTLog.mm gates on RCT_DEBUG || logFunction).
|
|
166
|
+
static void SBReportOffMainQueueSTTAccess(const char *where)
|
|
167
|
+
{
|
|
168
|
+
NSLog(@"[SpeechBridge] self.stt accessed off the main queue in %s. STT/DaVoiceTTS "
|
|
169
|
+
@"is main-queue-affine; see -methodQueue.", where);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// dispatch_assert_queue, not [NSThread isMainThread]: a dispatch_sync from main onto
|
|
173
|
+
// another serial queue runs on the main THREAD while off the main QUEUE, which
|
|
174
|
+
// isMainThread would incorrectly pass.
|
|
175
|
+
#if DEBUG
|
|
176
|
+
#define SBAssertSTTOnMainQueue() dispatch_assert_queue(dispatch_get_main_queue())
|
|
177
|
+
#else
|
|
178
|
+
#define SBAssertSTTOnMainQueue() \
|
|
179
|
+
do { \
|
|
180
|
+
if (![NSThread isMainThread]) { SBReportOffMainQueueSTTAccess(__PRETTY_FUNCTION__); } \
|
|
181
|
+
} while (0)
|
|
182
|
+
#endif
|
|
183
|
+
|
|
140
184
|
@interface SpeechBridge () <STTDelegate>
|
|
141
185
|
@property (nonatomic, strong, nullable) STT *stt;
|
|
142
186
|
@property (nonatomic, strong, nullable) DaVoiceTTS *tts;
|
|
@@ -152,6 +196,47 @@ static AVAudioPCMBuffer *SB_MakeMonoF32Buffer(NSData *raw,
|
|
|
152
196
|
|
|
153
197
|
@implementation SpeechBridge
|
|
154
198
|
|
|
199
|
+
// Explicit accessors so the main-queue guard covers every read and write of
|
|
200
|
+
// self.stt. Explicit accessors disable autosynthesis, hence the @synthesize.
|
|
201
|
+
@synthesize stt = _stt;
|
|
202
|
+
|
|
203
|
+
- (STT *)stt
|
|
204
|
+
{
|
|
205
|
+
SBAssertSTTOnMainQueue();
|
|
206
|
+
return _stt;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
- (void)setStt:(STT *)stt
|
|
210
|
+
{
|
|
211
|
+
SBAssertSTTOnMainQueue();
|
|
212
|
+
_stt = stt;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Single teardown path for the STT instance.
|
|
216
|
+
//
|
|
217
|
+
// Every call site used to be `[self.stt destroySpeech:nil]; self.stt = nil;`, which
|
|
218
|
+
// cleared the ivar only after the teardown call had already started, so other
|
|
219
|
+
// main-queue work could observe a half-destroyed STT through self.stt in between.
|
|
220
|
+
// Clearing the ivar first closes that window; the strong local keeps the instance
|
|
221
|
+
// alive for the duration of the call.
|
|
222
|
+
//
|
|
223
|
+
// The completion is deliberately nil, matching every existing call site. Passing a
|
|
224
|
+
// block that captures the instance would hand its lifetime to the framework: the
|
|
225
|
+
// final release would then happen wherever the framework drops the block, so
|
|
226
|
+
// -[STT dealloc] and the AVAudioEngine teardown inside it could run off the main
|
|
227
|
+
// queue -- the exact affinity violation this file exists to prevent, and it would
|
|
228
|
+
// not trip the guard above, because dealloc does not go through the property.
|
|
229
|
+
- (void)tearDownSTT
|
|
230
|
+
{
|
|
231
|
+
SBAssertSTTOnMainQueue();
|
|
232
|
+
STT *dyingSTT = _stt;
|
|
233
|
+
if (!dyingSTT) { return; }
|
|
234
|
+
_stt = nil;
|
|
235
|
+
@try {
|
|
236
|
+
[dyingSTT destroySpeech:nil];
|
|
237
|
+
} @catch (__unused id e) {}
|
|
238
|
+
}
|
|
239
|
+
|
|
155
240
|
RCT_EXPORT_MODULE(SpeechBridge)
|
|
156
241
|
|
|
157
242
|
// We emit the union of STT + TTS events
|
|
@@ -178,15 +263,42 @@ RCT_EXPORT_MODULE(SpeechBridge)
|
|
|
178
263
|
|
|
179
264
|
- (void)dealloc
|
|
180
265
|
{
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
266
|
+
// dealloc is not guaranteed to run on the main queue -- the last release can come
|
|
267
|
+
// from a bridge invalidation on any thread -- and [DaVoiceTTS destroy] /
|
|
268
|
+
// [STT destroySpeech:] are the same main-affine calls SBAssertSTTOnMainQueue exists
|
|
269
|
+
// to keep on main. Exempting dealloc from that rule would put full audio teardown
|
|
270
|
+
// off main at the one site where it is least recoverable, so hop instead. Only the
|
|
271
|
+
// locals are captured, never self, so this is safe from inside dealloc; the
|
|
272
|
+
// destroy order stays TTS then STT.
|
|
273
|
+
DaVoiceTTS *dyingTTS = _tts;
|
|
274
|
+
STT *dyingSTT = _stt;
|
|
275
|
+
_tts = nil;
|
|
276
|
+
_stt = nil;
|
|
277
|
+
if (!dyingTTS && !dyingSTT) { return; }
|
|
278
|
+
|
|
279
|
+
void (^teardown)(void) = ^{
|
|
280
|
+
@try { [dyingTTS destroy]; } @catch (__unused id e) {}
|
|
281
|
+
@try { [dyingSTT destroySpeech:nil]; } @catch (__unused id e) {}
|
|
282
|
+
};
|
|
283
|
+
if ([NSThread isMainThread]) {
|
|
284
|
+
teardown();
|
|
285
|
+
} else {
|
|
286
|
+
dispatch_async(dispatch_get_main_queue(), teardown);
|
|
287
|
+
}
|
|
184
288
|
}
|
|
185
289
|
|
|
186
290
|
#pragma mark - STTDelegate (forward all events)
|
|
187
291
|
|
|
188
292
|
- (void)stt:(STT *)stt didEmitEvent:(NSString *)name body:(NSDictionary *)body
|
|
189
293
|
{
|
|
294
|
+
// Ignore events from an STT we have already torn down. A dying instance stays our
|
|
295
|
+
// delegate until it deallocs, so without this check a trailing onSpeechStart from
|
|
296
|
+
// the old instance could latch sttEngineHot while the NEW recognizer (created by a
|
|
297
|
+
// fresh initAll()/initWithoutModel() racing the old instance's teardown) has not
|
|
298
|
+
// actually started -- reporting init success to JS before voice input is live.
|
|
299
|
+
// Compared against the ivar directly, not self.stt: this is invoked from the
|
|
300
|
+
// framework's own callback thread and must not trip the main-queue guard.
|
|
301
|
+
if (stt != _stt) { return; }
|
|
190
302
|
// Use the first onSpeechStart as the “engine hot” latch.
|
|
191
303
|
if ([name isEqualToString:@"onSpeechStart"] && !self.sttEngineHot) {
|
|
192
304
|
self.sttEngineHot = YES;
|
|
@@ -279,6 +391,16 @@ RCT_EXPORT_MODULE(SpeechBridge)
|
|
|
279
391
|
};
|
|
280
392
|
}
|
|
281
393
|
|
|
394
|
+
// Ticks on a private queue, delivers the verdict on main.
|
|
395
|
+
//
|
|
396
|
+
// The tick only reads self.sttEngineHot (atomic) and a clock -- no UIKit, no self.stt
|
|
397
|
+
// -- so it has no reason to occupy the main queue, while timeoutMs defaults to
|
|
398
|
+
// 7200000ms above, meaning an engine that never reports hot would otherwise leave a
|
|
399
|
+
// 50ms main-queue timer running for two hours. The completion is different: it is
|
|
400
|
+
// only ever invoked from initAll()/initWithoutModel(), where it writes
|
|
401
|
+
// self.initializing/self.initialized and resolves/rejects the caller's JS promise, so
|
|
402
|
+
// it is hopped back to main to keep that state single-threaded and to respect
|
|
403
|
+
// SBAssertSTTOnMainQueue on any self.stt access inside a caller's completion.
|
|
282
404
|
- (void)waitForSTTEngineHotWithTimeoutMs:(NSNumber *)timeoutMs
|
|
283
405
|
completion:(void (^)(BOOL ok))completion
|
|
284
406
|
{
|
|
@@ -287,26 +409,39 @@ RCT_EXPORT_MODULE(SpeechBridge)
|
|
|
287
409
|
|
|
288
410
|
CFTimeInterval deadline = CACurrentMediaTime() + MAX(0.1, t.doubleValue / 1000.0);
|
|
289
411
|
|
|
412
|
+
static dispatch_queue_t pollQueue;
|
|
413
|
+
static dispatch_once_t pollQueueOnce;
|
|
414
|
+
dispatch_once(&pollQueueOnce, ^{
|
|
415
|
+
pollQueue = dispatch_queue_create("com.davoice.SpeechBridge.enginehot", DISPATCH_QUEUE_SERIAL);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
void (^deliver)(BOOL) = ^(BOOL ok) {
|
|
419
|
+
dispatch_async(dispatch_get_main_queue(), ^{ completion(ok); });
|
|
420
|
+
};
|
|
421
|
+
|
|
290
422
|
__weak typeof(self) weakSelf = self;
|
|
291
423
|
__block void (^poll)(void) = ^{
|
|
292
424
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
293
425
|
if (!strongSelf) {
|
|
294
|
-
|
|
426
|
+
poll = nil;
|
|
427
|
+
deliver(NO);
|
|
295
428
|
return;
|
|
296
429
|
}
|
|
297
430
|
if (strongSelf.sttEngineHot) {
|
|
298
|
-
|
|
431
|
+
poll = nil;
|
|
432
|
+
deliver(YES);
|
|
299
433
|
return;
|
|
300
434
|
}
|
|
301
435
|
if (CACurrentMediaTime() >= deadline) {
|
|
302
|
-
|
|
436
|
+
poll = nil;
|
|
437
|
+
deliver(NO);
|
|
303
438
|
return;
|
|
304
439
|
}
|
|
305
440
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)),
|
|
306
|
-
|
|
441
|
+
pollQueue, poll);
|
|
307
442
|
};
|
|
308
443
|
|
|
309
|
-
dispatch_async(
|
|
444
|
+
dispatch_async(pollQueue, poll);
|
|
310
445
|
}
|
|
311
446
|
|
|
312
447
|
RCT_EXPORT_METHOD(hasMicPermissions:(RCTPromiseResolveBlock)resolve
|
|
@@ -536,8 +671,7 @@ RCT_EXPORT_METHOD(initAll:(NSDictionary *)opts
|
|
|
536
671
|
|
|
537
672
|
if (!modelURL) {
|
|
538
673
|
self.initializing = NO;
|
|
539
|
-
[self
|
|
540
|
-
self.stt = nil;
|
|
674
|
+
[self tearDownSTT];
|
|
541
675
|
reject(@"bad_model", [NSString stringWithFormat:@"Could not resolve model path: %@", modelPath], nil);
|
|
542
676
|
return;
|
|
543
677
|
}
|
|
@@ -545,8 +679,7 @@ RCT_EXPORT_METHOD(initAll:(NSDictionary *)opts
|
|
|
545
679
|
// Verify file exists for local file URLs (skip for bare .dm fallback — native core will search)
|
|
546
680
|
if (!isBareDmName && modelURL.isFileURL && ![[NSFileManager defaultManager] fileExistsAtPath:modelURL.path]) {
|
|
547
681
|
self.initializing = NO;
|
|
548
|
-
[self
|
|
549
|
-
self.stt = nil;
|
|
682
|
+
[self tearDownSTT];
|
|
550
683
|
reject(@"model_missing", [NSString stringWithFormat:@"Model file missing: %@", modelURL.path], nil);
|
|
551
684
|
return;
|
|
552
685
|
}
|
|
@@ -566,8 +699,7 @@ RCT_EXPORT_METHOD(initAll:(NSDictionary *)opts
|
|
|
566
699
|
if (err || !tts) {
|
|
567
700
|
self.initializing = NO;
|
|
568
701
|
self.sttEngineHot = NO;
|
|
569
|
-
[self
|
|
570
|
-
self.stt = nil;
|
|
702
|
+
[self tearDownSTT];
|
|
571
703
|
reject(@"tts_init_failed", err.localizedDescription ?: @"TTS init failed", err);
|
|
572
704
|
return;
|
|
573
705
|
}
|
|
@@ -611,8 +743,7 @@ RCT_EXPORT_METHOD(initWithoutModel:(NSDictionary *)opts
|
|
|
611
743
|
NSString *onboardingJsonPath = opts[@"onboardingJsonPath"];
|
|
612
744
|
NSNumber *timeoutMs = opts[@"timeoutMs"];
|
|
613
745
|
|
|
614
|
-
|
|
615
|
-
self.stt = nil;
|
|
746
|
+
[self tearDownSTT];
|
|
616
747
|
|
|
617
748
|
[self ensureSTT];
|
|
618
749
|
if (onboardingJsonPath && (id)onboardingJsonPath != [NSNull null] && onboardingJsonPath.length > 0) {
|
|
@@ -626,8 +757,7 @@ RCT_EXPORT_METHOD(initWithoutModel:(NSDictionary *)opts
|
|
|
626
757
|
self.initializing = NO;
|
|
627
758
|
self.initialized = NO;
|
|
628
759
|
self.sttEngineHot = NO;
|
|
629
|
-
|
|
630
|
-
self.stt = nil;
|
|
760
|
+
[self tearDownSTT];
|
|
631
761
|
reject(@"stt_init_timeout", @"STT did not become ready before timeout", nil);
|
|
632
762
|
return;
|
|
633
763
|
}
|
|
@@ -802,8 +932,7 @@ RCT_EXPORT_METHOD(destroyAll:(RCTPromiseResolveBlock)resolve
|
|
|
802
932
|
@try { [self.tts stopSpeaking]; [self.tts destroy]; } @catch (__unused id e) {}
|
|
803
933
|
self.tts = nil;
|
|
804
934
|
|
|
805
|
-
|
|
806
|
-
self.stt = nil;
|
|
935
|
+
[self tearDownSTT];
|
|
807
936
|
|
|
808
937
|
self.sttEngineHot = NO;
|
|
809
938
|
self.initialized = NO;
|
|
@@ -828,8 +957,7 @@ RCT_EXPORT_METHOD(destroyWihtouModel:(RCTPromiseResolveBlock)resolve
|
|
|
828
957
|
|
|
829
958
|
self.initializing = YES;
|
|
830
959
|
|
|
831
|
-
|
|
832
|
-
self.stt = nil;
|
|
960
|
+
[self tearDownSTT];
|
|
833
961
|
|
|
834
962
|
self.sttEngineHot = NO;
|
|
835
963
|
self.initialized = NO;
|
|
@@ -934,6 +1062,43 @@ RCT_EXPORT_METHOD(isRecognizing:(RCTResponseSenderBlock)callback)
|
|
|
934
1062
|
if (callback) callback(@[@(running ? 1 : 0)]);
|
|
935
1063
|
}
|
|
936
1064
|
|
|
1065
|
+
// Read-only diagnostics: captures AVAudioSession + STT/event-delivery state without
|
|
1066
|
+
// mutating anything, so it can be polled at any point in the lifecycle to compare a
|
|
1067
|
+
// healthy session against a stuck one.
|
|
1068
|
+
RCT_EXPORT_METHOD(sttHealthSnapshot:(RCTPromiseResolveBlock)resolve
|
|
1069
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
1070
|
+
{
|
|
1071
|
+
AVAudioSession *s = [AVAudioSession sharedInstance];
|
|
1072
|
+
NSMutableArray *inputs = [NSMutableArray array];
|
|
1073
|
+
for (AVAudioSessionPortDescription *p in s.currentRoute.inputs) { [inputs addObject:p.portType]; }
|
|
1074
|
+
NSMutableArray *outputs = [NSMutableArray array];
|
|
1075
|
+
for (AVAudioSessionPortDescription *p in s.currentRoute.outputs) { [outputs addObject:p.portType]; }
|
|
1076
|
+
resolve(@{
|
|
1077
|
+
// didEmitEvent: drops every STT callback when hasListeners is NO, and latches
|
|
1078
|
+
// sttEngineHot before that gate -- so the engine can report itself hot while JS
|
|
1079
|
+
// receives nothing. bridgePtr identifies which SpeechBridge instance is emitting,
|
|
1080
|
+
// to catch the case where JS subscribed to a different instance than the one
|
|
1081
|
+
// holding self.stt.
|
|
1082
|
+
@"hasListeners": @(self.hasListeners),
|
|
1083
|
+
@"sttEngineHot": @(self.sttEngineHot),
|
|
1084
|
+
@"bridgePtr": [NSString stringWithFormat:@"%p", self],
|
|
1085
|
+
@"hasStt": @(self.stt != nil),
|
|
1086
|
+
@"recognizing": @(self.stt ? [self.stt isRecognizing] : NO),
|
|
1087
|
+
@"initialized": @(self.initialized),
|
|
1088
|
+
@"initializing": @(self.initializing),
|
|
1089
|
+
@"category": s.category ?: @"",
|
|
1090
|
+
@"mode": s.mode ?: @"",
|
|
1091
|
+
@"categoryOptions": @(s.categoryOptions),
|
|
1092
|
+
@"inputAvailable": @(s.inputAvailable),
|
|
1093
|
+
@"sampleRate": @(s.sampleRate),
|
|
1094
|
+
@"inputChannels": @(s.inputNumberOfChannels),
|
|
1095
|
+
@"inputLatency": @(s.inputLatency),
|
|
1096
|
+
@"otherAudioPlaying": @(s.isOtherAudioPlaying),
|
|
1097
|
+
@"routeInputs": inputs,
|
|
1098
|
+
@"routeOutputs": outputs,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
937
1102
|
RCT_EXPORT_METHOD(setAECEnabled:(BOOL)enabled
|
|
938
1103
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
939
1104
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -997,35 +1162,83 @@ RCT_EXPORT_METHOD(playWav:(NSString *)pathOrURL
|
|
|
997
1162
|
return;
|
|
998
1163
|
}
|
|
999
1164
|
|
|
1000
|
-
|
|
1165
|
+
// Steps 5 and 6 below (verify + queue on the TTS engine), shared by every branch,
|
|
1166
|
+
// including the http(s) one after its asynchronous download completes.
|
|
1167
|
+
__weak typeof(self) weakSelf = self;
|
|
1168
|
+
void (^playLocalFile)(NSURL *) = ^(NSURL *fileURL) {
|
|
1169
|
+
typeof(self) strongSelf = weakSelf;
|
|
1170
|
+
if (!strongSelf || !strongSelf.tts) {
|
|
1171
|
+
reject(@"no_tts", @"TTS went away before playback", nil);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (!fileURL || ![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) {
|
|
1175
|
+
reject(@"file_missing", [NSString stringWithFormat:@"File missing: %@", fileURL.path], nil);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
SBDebugLog(@"[TTS] Playing file via DaVoiceTTS: %@", fileURL.path);
|
|
1179
|
+
[strongSelf.tts playWav:fileURL markAsLastUtterance:markAsLast.boolValue];
|
|
1180
|
+
resolve(@"queued");
|
|
1181
|
+
};
|
|
1001
1182
|
|
|
1002
|
-
// 1️⃣ Handle http(s) URLs — download to temporary file first
|
|
1183
|
+
// 1️⃣ Handle http(s) URLs — download to a cached temporary file first.
|
|
1184
|
+
//
|
|
1185
|
+
// This used to be a synchronous [NSData dataWithContentsOfURL:] on this module's
|
|
1186
|
+
// method queue, which is the main queue (see -methodQueue). In a debug build every
|
|
1187
|
+
// require()'d asset (e.g. a wake-word ping sound) resolves to a Metro dev-server
|
|
1188
|
+
// URL, so each ping blocked the UI for the whole request; on a slow or dropped link
|
|
1189
|
+
// to the Metro host that meant a frozen app for as long as pings kept arriving.
|
|
1190
|
+
// Download off the main queue with a bounded timeout, cache by URL so each asset is
|
|
1191
|
+
// fetched once, then hop back to the method queue to play it.
|
|
1003
1192
|
if ([pathOrURL hasPrefix:@"http://"] || [pathOrURL hasPrefix:@"https://"]) {
|
|
1004
|
-
SBDebugLog(@"[TTS] Downloading asset from URL: %@", pathOrURL);
|
|
1005
1193
|
NSURL *remoteURL = [NSURL URLWithString:pathOrURL];
|
|
1006
1194
|
if (!remoteURL) {
|
|
1007
1195
|
reject(@"bad_url", @"Invalid remote URL", nil);
|
|
1008
1196
|
return;
|
|
1009
1197
|
}
|
|
1010
1198
|
|
|
1011
|
-
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1199
|
+
NSString *cachedPath = SBCachedFilePathForRemoteAsset(pathOrURL);
|
|
1200
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:cachedPath]) {
|
|
1201
|
+
playLocalFile([NSURL fileURLWithPath:cachedPath]);
|
|
1014
1202
|
return;
|
|
1015
1203
|
}
|
|
1016
1204
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1205
|
+
SBDebugLog(@"[TTS] Downloading asset from URL: %@", pathOrURL);
|
|
1206
|
+
dispatch_queue_t methodQueue = [self methodQueue];
|
|
1207
|
+
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
|
1208
|
+
config.timeoutIntervalForRequest = 8.0;
|
|
1209
|
+
config.timeoutIntervalForResource = 15.0;
|
|
1210
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
|
1211
|
+
NSURLSessionDataTask *task = [session dataTaskWithURL:remoteURL
|
|
1212
|
+
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
1213
|
+
[session finishTasksAndInvalidate];
|
|
1214
|
+
NSInteger status = [response isKindOfClass:[NSHTTPURLResponse class]]
|
|
1215
|
+
? ((NSHTTPURLResponse *)response).statusCode : 200;
|
|
1216
|
+
BOOL downloaded = (error == nil && data.length > 0 && status < 400);
|
|
1217
|
+
BOOL written = downloaded && [data writeToFile:cachedPath atomically:YES];
|
|
1218
|
+
dispatch_async(methodQueue, ^{
|
|
1219
|
+
if (!downloaded) {
|
|
1220
|
+
NSString *why = error.localizedDescription ?: [NSString stringWithFormat:@"HTTP %ld", (long)status];
|
|
1221
|
+
reject(@"download_failed",
|
|
1222
|
+
[NSString stringWithFormat:@"Failed to download remote asset (%@)", why],
|
|
1223
|
+
error);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (!written) {
|
|
1227
|
+
reject(@"write_failed", @"Failed to write temporary file", nil);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
SBDebugLog(@"[TTS] Downloaded to cached file: %@", cachedPath);
|
|
1231
|
+
playLocalFile([NSURL fileURLWithPath:cachedPath]);
|
|
1232
|
+
});
|
|
1233
|
+
}];
|
|
1234
|
+
[task resume];
|
|
1235
|
+
return;
|
|
1025
1236
|
}
|
|
1026
1237
|
|
|
1238
|
+
NSURL *fileURL = nil;
|
|
1239
|
+
|
|
1027
1240
|
// 2️⃣ Handle bundled asset:/ paths (copied from main bundle)
|
|
1028
|
-
|
|
1241
|
+
if ([pathOrURL hasPrefix:@"asset:/"]) {
|
|
1029
1242
|
NSString *assetName = [pathOrURL stringByReplacingOccurrencesOfString:@"asset:/" withString:@""];
|
|
1030
1243
|
SBDebugLog(@"[TTS] Detected bundled asset: %@", assetName);
|
|
1031
1244
|
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:[assetName stringByDeletingPathExtension]
|
|
@@ -1057,16 +1270,8 @@ RCT_EXPORT_METHOD(playWav:(NSString *)pathOrURL
|
|
|
1057
1270
|
fileURL = [NSURL fileURLWithPath:pathOrURL];
|
|
1058
1271
|
}
|
|
1059
1272
|
|
|
1060
|
-
// 5️⃣ Verify existence
|
|
1061
|
-
|
|
1062
|
-
reject(@"file_missing", [NSString stringWithFormat:@"File missing: %@", fileURL.path], nil);
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// 6️⃣ Play through TTS engine (queued)
|
|
1067
|
-
SBDebugLog(@"[TTS] Playing file via DaVoiceTTS: %@", fileURL.path);
|
|
1068
|
-
[self.tts playWav:fileURL markAsLastUtterance:markAsLast.boolValue];
|
|
1069
|
-
resolve(@"queued");
|
|
1273
|
+
// 5️⃣ + 6️⃣ Verify existence and play through the TTS engine (queued)
|
|
1274
|
+
playLocalFile(fileURL);
|
|
1070
1275
|
}
|
|
1071
1276
|
|
|
1072
1277
|
/// playBuffer(desc: { base64, sampleRate, channels?, interleaved?, format: "i16" | "f32", markAsLast? })
|
|
@@ -8,32 +8,32 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>DavoiceTTS.framework/DavoiceTTS</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-
|
|
11
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>DavoiceTTS.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
15
15
|
<array>
|
|
16
16
|
<string>arm64</string>
|
|
17
|
+
<string>x86_64</string>
|
|
17
18
|
</array>
|
|
18
19
|
<key>SupportedPlatform</key>
|
|
19
20
|
<string>ios</string>
|
|
21
|
+
<key>SupportedPlatformVariant</key>
|
|
22
|
+
<string>simulator</string>
|
|
20
23
|
</dict>
|
|
21
24
|
<dict>
|
|
22
25
|
<key>BinaryPath</key>
|
|
23
26
|
<string>DavoiceTTS.framework/DavoiceTTS</string>
|
|
24
27
|
<key>LibraryIdentifier</key>
|
|
25
|
-
<string>ios-
|
|
28
|
+
<string>ios-arm64</string>
|
|
26
29
|
<key>LibraryPath</key>
|
|
27
30
|
<string>DavoiceTTS.framework</string>
|
|
28
31
|
<key>SupportedArchitectures</key>
|
|
29
32
|
<array>
|
|
30
33
|
<string>arm64</string>
|
|
31
|
-
<string>x86_64</string>
|
|
32
34
|
</array>
|
|
33
35
|
<key>SupportedPlatform</key>
|
|
34
36
|
<string>ios</string>
|
|
35
|
-
<key>SupportedPlatformVariant</key>
|
|
36
|
-
<string>simulator</string>
|
|
37
37
|
</dict>
|
|
38
38
|
</array>
|
|
39
39
|
<key>CFBundlePackageType</key>
|