Skip to content

Commit 51c0635

Browse files
committed
Implement the generateDialogue function for ios
1 parent c751964 commit 51c0635

7 files changed

Lines changed: 464 additions & 13 deletions

File tree

android/src/main/java/com/wfloat/WfloatModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class WfloatModule(reactContext: ReactApplicationContext) :
2727
)
2828
}
2929

30+
override fun generateDialogue(options: ReadableMap, promise: Promise) {
31+
promise.reject(
32+
"UNIMPLEMENTED",
33+
"generateDialogue is only implemented on iOS right now."
34+
)
35+
}
36+
3037
override fun play(promise: Promise) {
3138
promise.reject(
3239
"UNIMPLEMENTED",

example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1797,7 +1797,7 @@ SPEC CHECKSUMS:
17971797
React-Mapbuffer: 7eb5d69e1154e7743487ef0c8d7261e5b59afb32
17981798
React-microtasksnativemodule: 01dd998649ff5f8814846b7eee84c4d57f5d3671
17991799
react-native-blob-util: 5c9cd69d650932ed699ed51fe026a55aeb070bca
1800-
react-native-wfloat: 7d707ba2de6fcd676141a6240dbf417335411ead
1800+
react-native-wfloat: 298fb67b1ac6bdc87a311913e8a575a809b3b8dd
18011801
React-nativeconfig: f7ab6c152e780b99a8c17448f2d99cf5f69a2311
18021802
React-NativeModulesApple: 9aeb901b9bfcc9235e912445fb3cf4780a99baf4
18031803
React-perflogger: 16e049953d21b37e9871ddf0b02f414e12ff14ba

example/src/App.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react';
1+
import { useMemo, useState } from 'react';
22
import {
33
KeyboardAvoidingView,
44
Platform,
@@ -230,6 +230,66 @@ export default function App() {
230230
}
231231
};
232232

233+
const handleGenerateDialogue = async () => {
234+
const normalizedIntensity = clampUnit(Number(intensity), 0.5);
235+
const normalizedSpeed = parsePositiveNumber(speed, 1);
236+
const normalizedSilencePadding = parseNonNegativeNumber(
237+
silencePaddingSec,
238+
0.1
239+
);
240+
const normalizedVoiceId = normalizeVoiceIdInput(voiceId);
241+
242+
setStatus('generating');
243+
setProgressEvent(null);
244+
addLog('Generating two-segment dialogue.');
245+
246+
try {
247+
await SpeechClient.generateDialogue({
248+
segments: [
249+
{
250+
text,
251+
voiceId: normalizedVoiceId,
252+
emotion,
253+
intensity: normalizedIntensity,
254+
speed: normalizedSpeed,
255+
sentenceSilencePaddingSec: normalizedSilencePadding,
256+
},
257+
{
258+
text: 'This is a second dialogue segment using a different voice.',
259+
voiceId:
260+
selectedVoiceName === 'narrator_man'
261+
? 'narrator_woman'
262+
: 'narrator_man',
263+
emotion: 'joy',
264+
intensity: 0.55,
265+
speed: normalizedSpeed,
266+
sentenceSilencePaddingSec: normalizedSilencePadding,
267+
},
268+
],
269+
speed: normalizedSpeed,
270+
silenceBetweenSegmentsSec: 0.2,
271+
onProgressCallback: (event) => {
272+
setProgressEvent(event);
273+
},
274+
onFinishedPlayingCallback: () => {
275+
setStatus(SpeechClient.getStatus());
276+
addLog('Dialogue playback finished.', 'success');
277+
},
278+
});
279+
280+
const nextStatus = SpeechClient.getStatus();
281+
setStatus(nextStatus);
282+
addLog('Dialogue generation promise resolved.', 'success');
283+
} catch (error) {
284+
const nextStatus = SpeechClient.getStatus();
285+
setStatus(nextStatus);
286+
addLog(
287+
error instanceof Error ? error.message : 'Failed to generate dialogue.',
288+
'error'
289+
);
290+
}
291+
};
292+
233293
const handlePlay = async () => {
234294
try {
235295
await SpeechClient.play();
@@ -308,6 +368,7 @@ export default function App() {
308368
<View style={styles.buttonRow}>
309369
<ActionButton title="Load Model" onPress={handleLoadModel} />
310370
<ActionButton title="Generate" onPress={handleGenerate} />
371+
<ActionButton title="Dialogue" onPress={handleGenerateDialogue} />
311372
</View>
312373
<View style={styles.buttonRow}>
313374
<ActionButton title="Play" onPress={handlePlay} secondary />

ios/Wfloat.mm

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ typedef NS_ENUM(NSInteger, WfloatGenerateResult) {
2222
WfloatGenerateResultFailed = 2,
2323
};
2424

25+
@interface WfloatDialogueSegment : NSObject
26+
@property (nonatomic, copy) NSString *text;
27+
@property (nonatomic, assign) int32_t sid;
28+
@property (nonatomic, copy) NSString *emotion;
29+
@property (nonatomic, assign) float intensity;
30+
@property (nonatomic, assign) float speed;
31+
@property (nonatomic, assign) float sentenceSilencePaddingSec;
32+
@end
33+
34+
@implementation WfloatDialogueSegment
35+
@end
36+
37+
@interface WfloatPreparedDialogueSegment : NSObject
38+
@property (nonatomic, strong) NSArray<NSString *> *rawTextChunks;
39+
@property (nonatomic, strong) NSArray<NSString *> *textCleanChunks;
40+
@property (nonatomic, assign) int32_t sid;
41+
@property (nonatomic, assign) float speed;
42+
@property (nonatomic, assign) float sentenceSilencePaddingSec;
43+
@end
44+
45+
@implementation WfloatPreparedDialogueSegment
46+
@end
47+
2548
static NSString *WfloatCacheRootDirectory(void) {
2649
NSArray<NSString *> *paths =
2750
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
@@ -741,6 +764,97 @@ - (WfloatGenerateResult)generateSpeechForSession:(WfloatSpeechSession *)session
741764
return session.isCancelled ? WfloatGenerateResultCancelled : WfloatGenerateResultCompleted;
742765
}
743766

767+
- (WfloatGenerateResult)generateDialogueForSession:(WfloatSpeechSession *)session
768+
segments:(NSArray<WfloatDialogueSegment *> *)segments
769+
silenceBetweenSegmentsSec:(float)silenceBetweenSegmentsSec
770+
error:(NSError **)error {
771+
NSMutableArray<WfloatPreparedDialogueSegment *> *preparedSegments =
772+
[NSMutableArray arrayWithCapacity:segments.count];
773+
NSUInteger totalChunkCount = 0;
774+
775+
for (WfloatDialogueSegment *segment in segments) {
776+
if (session.isCancelled || ![self isCurrentSpeechSession:session]) {
777+
return WfloatGenerateResultCancelled;
778+
}
779+
780+
NSDictionary *preparedPayload = [self preparedTextPayloadForText:segment.text
781+
emotion:segment.emotion
782+
intensity:segment.intensity
783+
error:error];
784+
if (!preparedPayload) {
785+
return WfloatGenerateResultFailed;
786+
}
787+
788+
WfloatPreparedDialogueSegment *preparedSegment =
789+
[[WfloatPreparedDialogueSegment alloc] init];
790+
preparedSegment.rawTextChunks = [self stringArrayFromValue:preparedPayload[@"text"]];
791+
preparedSegment.textCleanChunks =
792+
[self stringArrayFromValue:preparedPayload[@"text_clean"]];
793+
preparedSegment.sid = segment.sid;
794+
preparedSegment.speed = segment.speed;
795+
preparedSegment.sentenceSilencePaddingSec = segment.sentenceSilencePaddingSec;
796+
[preparedSegments addObject:preparedSegment];
797+
totalChunkCount += preparedSegment.textCleanChunks.count;
798+
}
799+
800+
if (totalChunkCount == 0) {
801+
[session markGenerationComplete];
802+
return session.isCancelled ? WfloatGenerateResultCancelled : WfloatGenerateResultCompleted;
803+
}
804+
805+
NSUInteger progressIndex = 0;
806+
for (WfloatPreparedDialogueSegment *segment in preparedSegments) {
807+
for (NSUInteger index = 0; index < segment.textCleanChunks.count; index += 1) {
808+
if (session.isCancelled || ![self isCurrentSpeechSession:session]) {
809+
return WfloatGenerateResultCancelled;
810+
}
811+
812+
NSString *textClean = segment.textCleanChunks[index];
813+
const SherpaOnnxGeneratedAudio *generatedAudio =
814+
SherpaOnnxOfflineTtsGenerate(self.tts, textClean.UTF8String, segment.sid, segment.speed);
815+
if (!generatedAudio) {
816+
if (error) {
817+
*error = [NSError errorWithDomain:WfloatErrorDomain
818+
code:113
819+
userInfo:@{
820+
NSLocalizedDescriptionKey :
821+
@"Failed to generate speech audio from the prepared text.",
822+
}];
823+
}
824+
return WfloatGenerateResultFailed;
825+
}
826+
827+
progressIndex += 1;
828+
NSString *rawChunkText = index < segment.rawTextChunks.count ? segment.rawTextChunks[index] : @"";
829+
float silencePaddingSec = segment.sentenceSilencePaddingSec;
830+
if (index + 1 == segment.textCleanChunks.count) {
831+
silencePaddingSec = silenceBetweenSegmentsSec;
832+
}
833+
834+
NSError *scheduleError = nil;
835+
BOOL didSchedule = [session scheduleAudioSamples:generatedAudio->samples
836+
frameCount:generatedAudio->n
837+
progress:(double)progressIndex / (double)totalChunkCount
838+
text:rawChunkText
839+
highlightStart:0
840+
highlightEnd:1
841+
silencePaddingSec:silencePaddingSec
842+
error:&scheduleError];
843+
SherpaOnnxDestroyOfflineTtsGeneratedAudio(generatedAudio);
844+
845+
if (!didSchedule) {
846+
if (error) {
847+
*error = scheduleError;
848+
}
849+
return session.isCancelled ? WfloatGenerateResultCancelled : WfloatGenerateResultFailed;
850+
}
851+
}
852+
}
853+
854+
[session markGenerationComplete];
855+
return session.isCancelled ? WfloatGenerateResultCancelled : WfloatGenerateResultCompleted;
856+
}
857+
744858
- (void)cleanupStaleFilesInDirectory:(NSString *)directoryPath
745859
activeFileNames:(NSSet<NSString *> *)activeFileNames {
746860
NSError *contentsError = nil;
@@ -1017,6 +1131,147 @@ - (void)generate:(JS::NativeWfloat::GenerateNativeOptions &)options
10171131
});
10181132
}
10191133

1134+
- (void)generateDialogue:(JS::NativeWfloat::GenerateDialogueNativeOptions &)options
1135+
resolve:(RCTPromiseResolveBlock)resolve
1136+
reject:(RCTPromiseRejectBlock)reject {
1137+
if (!self.tts) {
1138+
reject(@"not_loaded",
1139+
@"SpeechClient is not created. Call SpeechClient.loadModel(...) first.",
1140+
nil);
1141+
return;
1142+
}
1143+
1144+
double requestIdValue = options.requestId();
1145+
auto nativeSegments = options.segments();
1146+
double silenceBetweenSegmentsSecValue = options.silenceBetweenSegmentsSec();
1147+
1148+
if (!isfinite(requestIdValue) || requestIdValue < 0 || floor(requestIdValue) != requestIdValue) {
1149+
reject(@"invalid_arguments", @"requestId must be a non-negative integer.", nil);
1150+
return;
1151+
}
1152+
1153+
if (nativeSegments.empty()) {
1154+
reject(@"invalid_arguments", @"segments is required.", nil);
1155+
return;
1156+
}
1157+
1158+
if (!isfinite(silenceBetweenSegmentsSecValue) || silenceBetweenSegmentsSecValue < 0) {
1159+
silenceBetweenSegmentsSecValue = 0.2;
1160+
}
1161+
1162+
NSMutableArray<WfloatDialogueSegment *> *segments =
1163+
[NSMutableArray arrayWithCapacity:(NSUInteger)nativeSegments.size()];
1164+
for (facebook::react::LazyVector<JS::NativeWfloat::GenerateDialogueNativeSegment>::size_type
1165+
index = 0;
1166+
index < nativeSegments.size();
1167+
index += 1) {
1168+
JS::NativeWfloat::GenerateDialogueNativeSegment nativeSegment = nativeSegments[index];
1169+
NSString *text = nativeSegment.text();
1170+
double sidValue = nativeSegment.sid();
1171+
NSString *emotion = nativeSegment.emotion();
1172+
double intensityValue = nativeSegment.intensity();
1173+
double speedValue = nativeSegment.speed();
1174+
double sentenceSilencePaddingSecValue = nativeSegment.sentenceSilencePaddingSec();
1175+
1176+
if (text.length == 0) {
1177+
reject(@"invalid_arguments",
1178+
[NSString stringWithFormat:@"segments[%d].text is required.", index],
1179+
nil);
1180+
return;
1181+
}
1182+
1183+
if (!isfinite(sidValue) || sidValue < 0 || floor(sidValue) != sidValue) {
1184+
reject(@"invalid_arguments", @"sid must be a non-negative integer.", nil);
1185+
return;
1186+
}
1187+
1188+
if (!isfinite(intensityValue)) {
1189+
intensityValue = 0.5;
1190+
}
1191+
1192+
if (!isfinite(speedValue) || speedValue <= 0) {
1193+
speedValue = 1.0;
1194+
}
1195+
1196+
if (!isfinite(sentenceSilencePaddingSecValue) ||
1197+
sentenceSilencePaddingSecValue < 0) {
1198+
sentenceSilencePaddingSecValue = 0.1;
1199+
}
1200+
1201+
WfloatDialogueSegment *segment = [[WfloatDialogueSegment alloc] init];
1202+
segment.text = text;
1203+
segment.sid = (int32_t)sidValue;
1204+
segment.emotion = emotion.length > 0 ? emotion : @"neutral";
1205+
segment.intensity = (float)MAX(0.0, MIN(intensityValue, 1.0));
1206+
segment.speed = (float)speedValue;
1207+
segment.sentenceSilencePaddingSec = (float)sentenceSilencePaddingSecValue;
1208+
[segments addObject:segment];
1209+
}
1210+
1211+
NSInteger requestId = (NSInteger)requestIdValue;
1212+
float silenceBetweenSegmentsSec = (float)silenceBetweenSegmentsSecValue;
1213+
int32_t sampleRate = SherpaOnnxOfflineTtsSampleRate(self.tts);
1214+
1215+
__weak Wfloat *weakSelf = self;
1216+
WfloatSpeechSession *session = [[WfloatSpeechSession alloc]
1217+
initWithRequestId:requestId
1218+
sampleRate:sampleRate
1219+
progressHandler:^(NSInteger progressRequestId,
1220+
double progress,
1221+
BOOL isPlaying,
1222+
NSInteger textHighlightStart,
1223+
NSInteger textHighlightEnd,
1224+
NSString *chunkText) {
1225+
[weakSelf emitSpeechProgressWithRequestId:progressRequestId
1226+
progress:progress
1227+
isPlaying:isPlaying
1228+
textHighlightStart:textHighlightStart
1229+
textHighlightEnd:textHighlightEnd
1230+
text:chunkText];
1231+
}
1232+
playbackFinishedHandler:^(NSInteger finishedRequestId) {
1233+
if (!weakSelf) {
1234+
return;
1235+
}
1236+
1237+
if (weakSelf.speechSession.requestId == finishedRequestId) {
1238+
weakSelf.speechSession = nil;
1239+
}
1240+
1241+
[weakSelf emitSpeechPlaybackFinishedWithRequestId:finishedRequestId];
1242+
}];
1243+
1244+
WfloatSpeechSession *previousSession = self.speechSession;
1245+
self.speechSession = session;
1246+
[previousSession cancel];
1247+
1248+
dispatch_async(self.workQueue, ^{
1249+
NSError *generationError = nil;
1250+
WfloatGenerateResult result =
1251+
[self generateDialogueForSession:session
1252+
segments:segments
1253+
silenceBetweenSegmentsSec:silenceBetweenSegmentsSec
1254+
error:&generationError];
1255+
1256+
dispatch_async(dispatch_get_main_queue(), ^{
1257+
if (result == WfloatGenerateResultFailed) {
1258+
if ([self isCurrentSpeechSession:session]) {
1259+
[self cancelCurrentSpeechSession];
1260+
} else {
1261+
[session cancel];
1262+
}
1263+
1264+
reject(@"generate_failed",
1265+
generationError.localizedDescription ?: @"Failed to generate speech audio.",
1266+
generationError);
1267+
return;
1268+
}
1269+
1270+
resolve(nil);
1271+
});
1272+
});
1273+
}
1274+
10201275
- (void)play:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
10211276
(void)reject;
10221277
[self.speechSession play];

react-native-wfloat.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616

1717
s.vendored_frameworks = "ios/onnxruntime.xcframework", "ios/sherpa-onnx.xcframework"
1818
s.source_files = "ios/**/*.{h,m,mm,cpp}"
19-
s.resources = "ios/resources/**/*"
19+
s.libraries = "AppleArchive"
2020
# s.private_header_files = "ios/**/*.h"
2121

2222
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.

0 commit comments

Comments
 (0)