diff --git a/CHANGELOG.md b/CHANGELOG.md
index 064399fdc..d33671415 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,16 @@ All notable changes to the **Sinch Java SDK** are documented in this file.
> - `[design]` — API design change
> - `[tech]` — technical improvement
+---
+## v2.4.0 - 2026-10-28
+
+### Voice
+- **[feature]** V2 helpers: `Destination`, SVAML commands (`Calls`, `Amd`, `Messages`, `Recording`, `CustomEvents`, `Menu`) and `CommandsSequenceCreator`. Event handlers accept a sequence lambda
+- **[feature]** V2 helpers: `CallBehaviorHelper` for services and `SinchEventResponses` for Sinch Events
+- **[feature]** V2 helpers: `VoiceName` catalog of text-to-speech voices
+- **[feature]** V2 `setCommands(...)` accepts one or more commands
+- **[doc]** V2 snippets, `sinch-events` example and README use the helpers
+
---
## v2.3.0 - 2026-10-15
diff --git a/README.md b/README.md
index 2397f8ffe..b31ffd7c3 100644
--- a/README.md
+++ b/README.md
@@ -369,6 +369,31 @@ You can find a complete example in [examples/sinch-events](https://github.com/si
The Voice API v2 needs no extra parameters, use the [common client](#client-initialization) shown above.
+The helpers in `com.sinch.sdk.domains.voice.models.v2.helpers` build the SVAML commands of a call, the call behavior of a service and the responses to Sinch Events. For example, to call a number, play a greeting once the call is answered, then hang up:
+
+```java
+import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
+import com.sinch.sdk.domains.voice.models.v2.helpers.CommandsSequenceCreator;
+
+StartCallRequest request =
+ StartCallRequest.builder()
+ .setCommands(
+ CommandsSequenceCreator.create()
+ .dial("RECIPIENT_PHONE_NUMBER", dial -> dial
+ .from("SINCH_VIRTUAL_PHONE_NUMBER")
+ .onAnswer(answered -> answered.text(
+ "Hello from the Sinch Java SDK!",
+ "Emma",
+ greeting -> greeting.onFinish(Calls.hangup()))))
+ .build())
+ .build();
+
+client.voice().v2().calls().start(request);
+```
+
+The same commands answer a Sinch Event, with `SinchEventResponses.response(...)` or, for a `call.incoming` event, `SinchEventResponses.incomingCallResponse(...)`.
+
### Your first request
diff --git a/client/src/main/com/sinch/sdk/SDK.java b/client/src/main/com/sinch/sdk/SDK.java
index 88ad4d526..1cc793ba6 100644
--- a/client/src/main/com/sinch/sdk/SDK.java
+++ b/client/src/main/com/sinch/sdk/SDK.java
@@ -3,6 +3,6 @@
public class SDK {
public static final String NAME = "Sinch Java SDK";
- public static final String VERSION = "2.4.0-dev";
+ public static final String VERSION = "2.5.0-dev";
public static final String AUXILIARY_FLAG = "";
}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Amd.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Amd.java
new file mode 100644
index 000000000..a3f08ccc7
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Amd.java
@@ -0,0 +1,24 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+/**
+ * Helpers to build the Answering Machine Detection SVAML command
+ *
+ * @since 2.4
+ */
+public final class Amd {
+
+ private Amd() {}
+
+ /**
+ * Detect what answered the call: a human, a machine, a beep, or an unknown result
+ *
+ *
Detection runs in parallel with the next commands. Set the commands to run for each result
+ * on the returned creator.
+ *
+ * @return The creator of the amd command
+ * @since 2.4
+ */
+ public static AmdCreator activate() {
+ return new AmdCreator();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/AmdCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/AmdCreator.java
new file mode 100644
index 000000000..9d8ca2204
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/AmdCreator.java
@@ -0,0 +1,200 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.amd.AmdCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.amd.AmdEvents;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of an amd command, returned by {@link Amd#activate()}
+ *
+ *
Calling an event handler more than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class AmdCreator {
+
+ private List onHuman;
+ private List onMachine;
+ private List onBeep;
+ private List onUnknown;
+
+ AmdCreator() {}
+
+ /**
+ * Add commands to run when a human answers
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onHuman(SvamlCommand... commands) {
+ onHuman = Commands.append(onHuman, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a human answers
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onHuman(Collection extends SvamlCommand> commands) {
+ onHuman = Commands.append(onHuman, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a human answers
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onHuman(Consumer sequence) {
+ onHuman = Commands.append(onHuman, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a machine answers
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onMachine(SvamlCommand... commands) {
+ onMachine = Commands.append(onMachine, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a machine answers
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onMachine(Collection extends SvamlCommand> commands) {
+ onMachine = Commands.append(onMachine, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a machine answers
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onMachine(Consumer sequence) {
+ onMachine = Commands.append(onMachine, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a beep is detected
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onBeep(SvamlCommand... commands) {
+ onBeep = Commands.append(onBeep, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a beep is detected
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onBeep(Collection extends SvamlCommand> commands) {
+ onBeep = Commands.append(onBeep, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when a beep is detected
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onBeep(Consumer sequence) {
+ onBeep = Commands.append(onBeep, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the result is unknown
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onUnknown(SvamlCommand... commands) {
+ onUnknown = Commands.append(onUnknown, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the result is unknown
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onUnknown(Collection extends SvamlCommand> commands) {
+ onUnknown = Commands.append(onUnknown, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the result is unknown
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public AmdCreator onUnknown(Consumer sequence) {
+ onUnknown = Commands.append(onUnknown, sequence);
+ return this;
+ }
+
+ /**
+ * Build the amd command
+ *
+ * @return The command. Its events are only set when a handler was given
+ * @since 2.4
+ */
+ public AmdCommand build() {
+ AmdCommand.Builder builder = AmdCommand.builder();
+ if (null != onHuman || null != onMachine || null != onBeep || null != onUnknown) {
+ builder.setEvents(buildEvents());
+ }
+ return builder.build();
+ }
+
+ private AmdEvents buildEvents() {
+ AmdEvents.Builder builder = AmdEvents.builder();
+ if (null != onHuman) {
+ builder.setOnHuman(Commands.copy(onHuman));
+ }
+ if (null != onMachine) {
+ builder.setOnMachine(Commands.copy(onMachine));
+ }
+ if (null != onBeep) {
+ builder.setOnBeep(Commands.copy(onBeep));
+ }
+ if (null != onUnknown) {
+ builder.setOnUnknown(Commands.copy(onUnknown));
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/BaseDestination.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/BaseDestination.java
new file mode 100644
index 000000000..b646f4b3c
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/BaseDestination.java
@@ -0,0 +1,15 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin;
+
+/**
+ * Call endpoint built by {@link Destination#of(String)}
+ *
+ * Valid as to ({@link CallDestination}) and from ({@link CallOrigin}).
+ *
+ *
A stream endpoint is only valid as to: the API rejects it as from.
+ *
+ * @since 2.4
+ */
+public interface BaseDestination extends CallOrigin, CallDestination {}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelper.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelper.java
new file mode 100644
index 000000000..bf0519e0a
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelper.java
@@ -0,0 +1,102 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehavior;
+import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationConfiguration;
+import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Helpers to build the call behavior of a Voice V2 service, used to create or update it
+ *
+ * @since 2.4
+ */
+public final class CallBehaviorHelper {
+
+ private CallBehaviorHelper() {}
+
+ /**
+ * No call behavior: incoming calls are not handled. Outbound calls can still be started with the
+ * API
+ *
+ * @return The NONE call behavior
+ * @since 2.4
+ */
+ public static NoneCallBehavior none() {
+ return NoneCallBehavior.NONE_CALL_BEHAVIOR;
+ }
+
+ /**
+ * Calls are handled by sending Sinch Events to the backend, which answers with the commands that
+ * control the call
+ *
+ * @param url URL the events are sent to
+ * @return The event destination call behavior, sent as WEBHOOK
+ * @since 2.4
+ */
+ public static EventDestinationCallBehavior eventDestination(String url) {
+ Objects.requireNonNull(url, "url must not be null");
+ return EventDestinationCallBehavior.builder()
+ .setEventDestination(EventDestinationConfiguration.builder().setUrl(url).build())
+ .build();
+ }
+
+ /**
+ * Calls are handled by sending Sinch Events to the backend, with a fallback URL
+ *
+ * @param url URL the events are sent to
+ * @param fallbackUrl URL a failed request is sent to
+ * @return The event destination call behavior, sent as WEBHOOK
+ * @see #eventDestination(String)
+ * @since 2.4
+ */
+ public static EventDestinationCallBehavior eventDestination(String url, String fallbackUrl) {
+ Objects.requireNonNull(url, "url must not be null");
+ Objects.requireNonNull(fallbackUrl, "fallbackUrl must not be null");
+ return EventDestinationCallBehavior.builder()
+ .setEventDestination(
+ EventDestinationConfiguration.builder().setUrl(url).setFallbackUrl(fallbackUrl).build())
+ .build();
+ }
+
+ /**
+ * Calls are handled by the same commands, with no backend involved
+ *
+ *
Named staticBehavior: static is a reserved word in Java.
+ *
+ * @param commands Commands run for every call of the service
+ * @return The creator of the STATIC call behavior
+ * @since 2.4
+ */
+ public static StaticCallBehaviorCreator staticBehavior(SvamlCommand... commands) {
+ return new StaticCallBehaviorCreator(Commands.append(null, commands));
+ }
+
+ /**
+ * Calls are handled by the same commands, with no backend involved
+ *
+ * @param commands Commands run for every call of the service
+ * @return The creator of the STATIC call behavior
+ * @see #staticBehavior(SvamlCommand...)
+ * @since 2.4
+ */
+ public static StaticCallBehaviorCreator staticBehavior(
+ Collection extends SvamlCommand> commands) {
+ return new StaticCallBehaviorCreator(Commands.append(null, commands));
+ }
+
+ /**
+ * Calls are handled by the same commands, with no backend involved
+ *
+ * @param sequence Lambda adding the commands run for every call of the service
+ * @return The creator of the STATIC call behavior
+ * @see #staticBehavior(SvamlCommand...)
+ * @since 2.4
+ */
+ public static StaticCallBehaviorCreator staticBehavior(
+ Consumer sequence) {
+ return new StaticCallBehaviorCreator(Commands.append(null, sequence));
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Calls.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Calls.java
new file mode 100644
index 000000000..b52c1333a
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Calls.java
@@ -0,0 +1,95 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.AnswerCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.BridgeCallCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.PauseCommand;
+import java.util.Objects;
+
+/**
+ * Helpers to build the call control SVAML commands
+ *
+ * @since 2.4
+ */
+public final class Calls {
+
+ private Calls() {}
+
+ /**
+ * Answer the current call leg
+ *
+ * @return The answer command
+ * @since 2.4
+ */
+ public static AnswerCommand answer() {
+ return AnswerCommand.ANSWER_COMMAND;
+ }
+
+ /**
+ * Join a bridge, created if no bridge with this name exists in the session
+ *
+ * @param name Name of the bridge, sent as bridgeName
+ * @return The bridgeCall command
+ * @since 2.4
+ */
+ public static BridgeCallCommand bridgeCall(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ return BridgeCallCommand.builder().setBridgeName(name).build();
+ }
+
+ /**
+ * Start a new outbound call leg
+ *
+ * @param to Call destination
+ * @return The creator of the dial command
+ * @since 2.4
+ */
+ public static DialCreator dial(CallDestination to) {
+ return new DialCreator(to);
+ }
+
+ /**
+ * Start a new outbound call leg
+ *
+ * @param to Call destination, parsed with {@link Destination#of(String)}
+ * @return The creator of the dial command
+ * @since 2.4
+ */
+ public static DialCreator dial(String to) {
+ return new DialCreator(Destination.of(to));
+ }
+
+ /**
+ * End the current call leg
+ *
+ * @return The hangup command
+ * @since 2.4
+ */
+ public static HangupCommand hangup() {
+ return HangupCommand.HANGUP_COMMAND;
+ }
+
+ /**
+ * End a call leg
+ *
+ * @param name Name of the call leg to end, sent as callName
+ * @return The hangup command
+ * @since 2.4
+ */
+ public static HangupCommand hangup(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ return HangupCommand.builder().setCallName(name).build();
+ }
+
+ /**
+ * Delay the next command in the sequence. The call audio is not affected
+ *
+ * @param durationMilliseconds Duration of the pause in milliseconds
+ * @return The pause command
+ * @since 2.4
+ */
+ public static PauseCommand pause(int durationMilliseconds) {
+ return PauseCommand.builder().setDurationMilliseconds(durationMilliseconds).build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Commands.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Commands.java
new file mode 100644
index 000000000..f4d12584f
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Commands.java
@@ -0,0 +1,48 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/** Collects the commands given to the event handlers of the creators */
+final class Commands {
+
+ private Commands() {}
+
+ /**
+ * Append commands to a list, creating it on first use. Calling with no command still creates the
+ * list, so an explicitly empty handler is sent as an empty list.
+ */
+ static List append(List current, SvamlCommand... commands) {
+ Objects.requireNonNull(commands, "commands must not be null");
+ return append(current, Arrays.asList(commands));
+ }
+
+ static List append(
+ List current, Collection extends SvamlCommand> commands) {
+ Objects.requireNonNull(commands, "commands must not be null");
+ List list = null == current ? new ArrayList<>() : current;
+ for (SvamlCommand command : commands) {
+ list.add(Objects.requireNonNull(command, "command must not be null"));
+ }
+ return list;
+ }
+
+ /** Append the commands added by a lambda to a new sequence */
+ static List append(
+ List current, Consumer sequence) {
+ Objects.requireNonNull(sequence, "sequence must not be null");
+ CommandsSequenceCreator creator = new CommandsSequenceCreator();
+ sequence.accept(creator);
+ return append(current, creator.commands());
+ }
+
+ /** Copy taken at build time, so a creator reused after build() does not alias it */
+ static List copy(List commands) {
+ return null == commands ? null : new ArrayList<>(commands);
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreator.java
new file mode 100644
index 000000000..12a167157
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreator.java
@@ -0,0 +1,523 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingOptions;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of an ordered sequence of SVAML commands, e.g. the commands of a call
+ * or of a Sinch Event response
+ *
+ * Each method adds one command, built with the matching helper: e.g. amd uses
+ * {@link Amd} and recording uses {@link Recording}.
+ *
+ *
The creators of the commands with events are given to a lambda, so they need no intermediate
+ * build:
+ *
+ *
{@code
+ * List commands =
+ * CommandsSequenceCreator.create()
+ * .dial("+34600111222", dial -> dial
+ * .from("+34910000000")
+ * .onAnswer(answered -> answered
+ * .text("Connecting you to support", "Emma")
+ * .bridgeCall("support-queue")))
+ * .build();
+ * }
+ *
+ * The event handlers of every creator also accept such a lambda.
+ *
+ * @since 2.4
+ */
+public final class CommandsSequenceCreator {
+
+ private final List commands = new ArrayList<>();
+
+ CommandsSequenceCreator() {}
+
+ /**
+ * Start a new sequence
+ *
+ * @return The creator
+ * @since 2.4
+ */
+ public static CommandsSequenceCreator create() {
+ return new CommandsSequenceCreator();
+ }
+
+ /**
+ * Add an answer command
+ *
+ * @return This creator
+ * @see Calls#answer()
+ * @since 2.4
+ */
+ public CommandsSequenceCreator answer() {
+ return command(Calls.answer());
+ }
+
+ /**
+ * Add a bridgeCall command
+ *
+ * @param name Name of the bridge
+ * @return This creator
+ * @see Calls#bridgeCall(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator bridgeCall(String name) {
+ return command(Calls.bridgeCall(name));
+ }
+
+ /**
+ * Add a dial command with no other field than its destination
+ *
+ * @param to Call destination
+ * @return This creator
+ * @see Calls#dial(CallDestination)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator dial(CallDestination to) {
+ return command(Calls.dial(to).build());
+ }
+
+ /**
+ * Add a dial command with no other field than its destination
+ *
+ * @param to Call destination, parsed with {@link Destination#of(String)}
+ * @return This creator
+ * @see Calls#dial(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator dial(String to) {
+ return command(Calls.dial(to).build());
+ }
+
+ /**
+ * Add a dial command
+ *
+ * @param to Call destination
+ * @param dial Lambda filling the other fields and the events
+ * @return This creator
+ * @see Calls#dial(CallDestination)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator dial(CallDestination to, Consumer dial) {
+ DialCreator creator = Calls.dial(to);
+ Objects.requireNonNull(dial, "dial must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a dial command
+ *
+ * @param to Call destination, parsed with {@link Destination#of(String)}
+ * @param dial Lambda filling the other fields and the events
+ * @return This creator
+ * @see Calls#dial(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator dial(String to, Consumer dial) {
+ return dial(Destination.of(to), dial);
+ }
+
+ /**
+ * Add a hangup command ending the current call leg
+ *
+ * @return This creator
+ * @see Calls#hangup()
+ * @since 2.4
+ */
+ public CommandsSequenceCreator hangup() {
+ return command(Calls.hangup());
+ }
+
+ /**
+ * Add a hangup command
+ *
+ * @param name Name of the call leg to end
+ * @return This creator
+ * @see Calls#hangup(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator hangup(String name) {
+ return command(Calls.hangup(name));
+ }
+
+ /**
+ * Add a pause command
+ *
+ * @param durationMilliseconds Duration of the pause in milliseconds
+ * @return This creator
+ * @see Calls#pause(int)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator pause(int durationMilliseconds) {
+ return command(Calls.pause(durationMilliseconds));
+ }
+
+ /**
+ * Add an amd command
+ *
+ * @param amd Lambda filling the events
+ * @return This creator
+ * @see Amd#activate()
+ * @since 2.4
+ */
+ public CommandsSequenceCreator amd(Consumer amd) {
+ AmdCreator creator = Amd.activate();
+ Objects.requireNonNull(amd, "amd must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @see Messages#text(String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator text(String text, String voiceName) {
+ return command(Messages.text(text, voiceName));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @see Messages#text(String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator text(String text, VoiceName voiceName) {
+ return text(text, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in SSML
+ *
+ * @param ssml SSML document to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @see Messages#ssml(String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator ssml(String ssml, String voiceName) {
+ return command(Messages.ssml(ssml, voiceName));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in SSML
+ *
+ * @param ssml SSML document to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @see Messages#ssml(String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator ssml(String ssml, VoiceName voiceName) {
+ return ssml(ssml, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add a messages command playing an audio file
+ *
+ * @param url URL of the media to play
+ * @return This creator
+ * @see Messages#play(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator play(String url) {
+ return command(Messages.play(url));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in plain text, with a name
+ * or commands run once it is played
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return This creator
+ * @see Messages#text(String, String, Consumer)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator text(
+ String text, String voiceName, Consumer messages) {
+ return command(Messages.text(text, voiceName, messages));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in plain text, with a name
+ * or commands run once it is played
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return This creator
+ * @see Messages#text(String, String, Consumer)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator text(
+ String text, VoiceName voiceName, Consumer messages) {
+ return text(text, VoiceName.valueOf(voiceName), messages);
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in SSML, with a name or
+ * commands run once it is played
+ *
+ * @param ssml SSML document to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return This creator
+ * @see Messages#ssml(String, String, Consumer)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator ssml(
+ String ssml, String voiceName, Consumer messages) {
+ return command(Messages.ssml(ssml, voiceName, messages));
+ }
+
+ /**
+ * Add a messages command playing a text-to-speech message in SSML, with a name or
+ * commands run once it is played
+ *
+ * @param ssml SSML document to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return This creator
+ * @see Messages#ssml(String, String, Consumer)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator ssml(
+ String ssml, VoiceName voiceName, Consumer messages) {
+ return ssml(ssml, VoiceName.valueOf(voiceName), messages);
+ }
+
+ /**
+ * Add a messages command playing an audio file, with a name or commands run once it
+ * is played
+ *
+ * @param url URL of the media to play
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return This creator
+ * @see Messages#play(String, Consumer)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator play(String url, Consumer messages) {
+ return command(Messages.play(url, messages));
+ }
+
+ /**
+ * Add a messages command
+ *
+ * @param messages Lambda adding the messages, the name and the events
+ * @return This creator
+ * @see Messages#start()
+ * @since 2.4
+ */
+ public CommandsSequenceCreator messages(Consumer messages) {
+ MessagesCreator creator = Messages.start();
+ Objects.requireNonNull(messages, "messages must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a stopMessages command
+ *
+ * @param name Name of the message sequence
+ * @return This creator
+ * @see Messages#stop(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator stopMessages(String name) {
+ return command(Messages.stop(name));
+ }
+
+ /**
+ * Add a startRecording command with no other field than its recording options
+ *
+ * @param destination Third party storage the recording is delivered to
+ * @param url Destination URL
+ * @param credentials Credentials to the third party storage
+ * @return This creator
+ * @see Recording#start(RecordingDestinationType, String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator recording(
+ RecordingDestinationType destination, String url, String credentials) {
+ return command(Recording.start(destination, url, credentials).build());
+ }
+
+ /**
+ * Add a startRecording command
+ *
+ * @param destination Third party storage the recording is delivered to
+ * @param url Destination URL
+ * @param credentials Credentials to the third party storage
+ * @param recording Lambda filling the other fields and the events
+ * @return This creator
+ * @see Recording#start(RecordingDestinationType, String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator recording(
+ RecordingDestinationType destination,
+ String url,
+ String credentials,
+ Consumer recording) {
+ RecordingCreator creator = Recording.start(destination, url, credentials);
+ Objects.requireNonNull(recording, "recording must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a startRecording command with no other field than its recording options
+ *
+ * @param options Recording options
+ * @return This creator
+ * @see Recording#start(RecordingOptions)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator recording(RecordingOptions options) {
+ return command(Recording.start(options).build());
+ }
+
+ /**
+ * Add a startRecording command
+ *
+ * @param options Recording options
+ * @param recording Lambda filling the other fields and the events
+ * @return This creator
+ * @see Recording#start(RecordingOptions)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator recording(
+ RecordingOptions options, Consumer recording) {
+ RecordingCreator creator = Recording.start(options);
+ Objects.requireNonNull(recording, "recording must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a stopRecording command
+ *
+ * @param name Name of the recording
+ * @return This creator
+ * @see Recording#stop(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator stopRecording(String name) {
+ return command(Recording.stop(name));
+ }
+
+ /**
+ * Add a customEvent command
+ *
+ * @param name Name of the custom event
+ * @param url URL the event is sent to
+ * @return This creator
+ * @see CustomEvents#trigger(String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator customEvents(String name, String url) {
+ return command(CustomEvents.trigger(name, url));
+ }
+
+ /**
+ * Add a customEvent command with a fallback URL
+ *
+ * @param name Name of the custom event
+ * @param url URL the event is sent to
+ * @param fallbackUrl URL a failed request is sent to
+ * @return This creator
+ * @see CustomEvents#trigger(String, String, String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator customEvents(String name, String url, String fallbackUrl) {
+ return command(CustomEvents.trigger(name, url, fallbackUrl));
+ }
+
+ /**
+ * Add a menu command
+ *
+ * @param startMenu Name of the item played first
+ * @param menu Lambda adding the items
+ * @return This creator
+ * @see Menu#create(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator menu(String startMenu, Consumer menu) {
+ MenuCreator creator = Menu.create(startMenu);
+ Objects.requireNonNull(menu, "menu must not be null").accept(creator);
+ return command(creator.build());
+ }
+
+ /**
+ * Add a gotoMenu command. Only valid within a menu
+ *
+ * @param name Name of the menu item
+ * @return This creator
+ * @see Menu#gotoMenu(String)
+ * @since 2.4
+ */
+ public CommandsSequenceCreator gotoMenu(String name) {
+ return command(Menu.gotoMenu(name));
+ }
+
+ /**
+ * Add commands built by other means, e.g. with the model builders
+ *
+ * @param commands Commands to add
+ * @return This creator
+ * @since 2.4
+ */
+ public CommandsSequenceCreator command(SvamlCommand... commands) {
+ Commands.append(this.commands, commands);
+ return this;
+ }
+
+ /**
+ * Add commands built by other means, e.g. with the model builders
+ *
+ * @param commands Commands to add
+ * @return This creator
+ * @since 2.4
+ */
+ public CommandsSequenceCreator command(Collection extends SvamlCommand> commands) {
+ Commands.append(this.commands, commands);
+ return this;
+ }
+
+ /**
+ * Build the sequence
+ *
+ * @return A new list with the commands, in the order they were added
+ * @throws IllegalStateException if a gotoMenu command is outside a menu, or if a
+ * menu of the sequence has a start menu or a gotoMenu target that is not one of
+ * its items
+ * @since 2.4
+ */
+ public List build() {
+ MenuCreator.checkGotoTargets(commands, Collections.emptySet());
+ return commands();
+ }
+
+ /**
+ * The commands, not checked: a sequence filled in an event handler lambda may sit within a menu
+ * whose items are not known here. The enclosing menu or sequence checks them when built
+ */
+ List commands() {
+ return new ArrayList<>(commands);
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEvents.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEvents.java
new file mode 100644
index 000000000..f425c765b
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEvents.java
@@ -0,0 +1,55 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.customevents.CustomEventCommand;
+import java.util.Objects;
+
+/**
+ * Helpers to build the custom event SVAML command
+ *
+ * @since 2.4
+ */
+public final class CustomEvents {
+
+ private CustomEvents() {}
+
+ /**
+ * Send a mid-call event to the application backend, which answers with the commands that control
+ * the rest of the call
+ *
+ * The sequence waits for the backend answer before running the next command.
+ *
+ *
The backend receives a call.webhook.<name> event. Once parsed by the SDK,
+ * its type is call.customEvent.<name>.
+ *
+ * @param name Name of the custom event, sent as webhookName
+ * @param url URL the event is sent to
+ * @return The customEvent command
+ * @since 2.4
+ */
+ public static CustomEventCommand trigger(String name, String url) {
+ Objects.requireNonNull(name, "name must not be null");
+ Objects.requireNonNull(url, "url must not be null");
+ return CustomEventCommand.builder().setCustomEventName(name).setUrl(url).build();
+ }
+
+ /**
+ * Send a mid-call event to the application backend, with a fallback URL
+ *
+ * @param name Name of the custom event, sent as webhookName
+ * @param url URL the event is sent to
+ * @param fallbackUrl URL a failed request is sent to
+ * @return The customEvent command
+ * @see #trigger(String, String)
+ * @since 2.4
+ */
+ public static CustomEventCommand trigger(String name, String url, String fallbackUrl) {
+ Objects.requireNonNull(name, "name must not be null");
+ Objects.requireNonNull(url, "url must not be null");
+ Objects.requireNonNull(fallbackUrl, "fallbackUrl must not be null");
+ return CustomEventCommand.builder()
+ .setCustomEventName(name)
+ .setUrl(url)
+ .setFallbackUrl(fallbackUrl)
+ .build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Destination.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Destination.java
new file mode 100644
index 000000000..10eab01ab
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Destination.java
@@ -0,0 +1,294 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin;
+import com.sinch.sdk.domains.voice.models.v2.destination.Phone;
+import com.sinch.sdk.domains.voice.models.v2.destination.Sip;
+import com.sinch.sdk.domains.voice.models.v2.destination.SipFrom;
+import com.sinch.sdk.domains.voice.models.v2.destination.Stream;
+import com.sinch.sdk.domains.voice.models.v2.destination.VoiceRelay;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Factory helpers for the to and from values of a Voice V2 call
+ *
+ *
to takes a {@link CallDestination} and from a {@link CallOrigin}.
+ * {@link #of(String)} returns a {@link BaseDestination}, accepted by both.
+ *
+ *
Each factory takes the required fields. Its lambda overload sets the optional ones on the
+ * model builder, e.g. sip(endpoint, sip -> sip.setTransport(...)).
+ *
+ * @since 2.4
+ */
+public final class Destination {
+
+ static final String PHONE_PREFIX = "phone:";
+ static final String STREAM_PREFIX = "stream:";
+ static final String SIP_PREFIX = "sip:";
+ static final String SIPS_PREFIX = "sips:";
+
+ private Destination() {}
+
+ /**
+ * Create a call endpoint from a string, detecting its type from the prefix
+ *
+ *
+ * sip: and sips:: SIP endpoint, the URI is kept as given
+ * stream:: stream endpoint (only valid as to), prefix removed
+ * phone:: phone number, the prefix is removed
+ * - Anything else: phone number, the value is kept as given (e.g. a batch placeholder such as
+ *
@toNumber)
+ *
+ *
+ * The result is accepted as both to and from. It only carries the
+ * fields found in the string: use the dedicated factories to set optional fields.
+ *
+ *
A voice relay destination is not built here, as it requires a TTS voice and an STT language:
+ * use the voiceRelay factories.
+ *
+ *
The result is meant to build requests. It wraps the generated model and serializes as it,
+ * but it is neither an instance of {@link Phone}, {@link Sip} or {@link Stream}, nor equal to the
+ * model returned by the dedicated factories. Use them when the typed model is needed.
+ *
+ * @param value Endpoint, optionally prefixed with its type
+ * @return The phone, SIP or stream endpoint
+ * @since 2.4
+ */
+ public static BaseDestination of(String value) {
+ Objects.requireNonNull(value, "value must not be null");
+ if (isSip(value)) {
+ return new Endpoint(sip(value));
+ }
+ if (value.startsWith(STREAM_PREFIX)) {
+ return new Endpoint(stream(value.substring(STREAM_PREFIX.length())));
+ }
+ return new Endpoint(phone(withoutPhonePrefix(value)));
+ }
+
+ /**
+ * Create a phone number on the PSTN, valid as both call destination and call origin
+ *
+ * @param number Phone number in E.164 format
+ * @return The phone destination
+ * @since 2.4
+ */
+ public static Phone phone(String number) {
+ Objects.requireNonNull(number, "number must not be null");
+ return Phone.builder().setNumber(number).build();
+ }
+
+ /**
+ * Create a SIP call destination. Only valid as call destination: use {@link #sipFrom(String)} for
+ * a call origin
+ *
+ * @param endpoint SIP URI of the destination endpoint (sip: or sips:
+ * scheme)
+ * @return The SIP destination
+ * @since 2.4
+ */
+ public static Sip sip(String endpoint) {
+ return sip(endpoint, sip -> {});
+ }
+
+ /**
+ * Create a SIP call destination with optional fields, e.g. the transport or the call headers
+ *
+ * @param endpoint SIP URI of the destination endpoint (sip: or sips:
+ * scheme)
+ * @param sip Lambda setting the optional fields on the model builder, which already holds the
+ * endpoint
+ * @return The SIP destination
+ * @since 2.4
+ */
+ public static Sip sip(String endpoint, Consumer sip) {
+ Objects.requireNonNull(endpoint, "endpoint must not be null");
+ Sip.Builder builder = Sip.builder().setEndpoint(endpoint);
+ Objects.requireNonNull(sip, "sip must not be null").accept(builder);
+ return builder.build();
+ }
+
+ /**
+ * Create a SIP call origin. Only valid as call origin: use {@link #sip(String)} for a call
+ * destination
+ *
+ * @param endpoint SIP URI of the originating endpoint (sip: or sips:
+ * scheme)
+ * @return The SIP origin
+ * @since 2.4
+ */
+ public static SipFrom sipFrom(String endpoint) {
+ return sipFrom(endpoint, sipFrom -> {});
+ }
+
+ /**
+ * Create a SIP call origin with optional fields, e.g. the display name
+ *
+ * @param endpoint SIP URI of the originating endpoint (sip: or sips:
+ * scheme)
+ * @param sipFrom Lambda setting the optional fields on the model builder, which already holds the
+ * endpoint
+ * @return The SIP origin
+ * @since 2.4
+ */
+ public static SipFrom sipFrom(String endpoint, Consumer sipFrom) {
+ Objects.requireNonNull(endpoint, "endpoint must not be null");
+ SipFrom.Builder builder = SipFrom.builder().setEndpoint(endpoint);
+ Objects.requireNonNull(sipFrom, "sipFrom must not be null").accept(builder);
+ return builder.build();
+ }
+
+ /**
+ * Create a WebSocket stream call destination
+ *
+ * @param endpoint WebSocket URL (ws:// or wss://) accepting the
+ * connection
+ * @return The stream destination
+ * @since 2.4
+ */
+ public static Stream stream(String endpoint) {
+ return stream(endpoint, stream -> {});
+ }
+
+ /**
+ * Create a WebSocket stream call destination with optional fields, e.g. the stream options or the
+ * call headers
+ *
+ * @param endpoint WebSocket URL (ws:// or wss://) accepting the
+ * connection
+ * @param stream Lambda setting the optional fields on the model builder, which already holds the
+ * endpoint
+ * @return The stream destination
+ * @since 2.4
+ */
+ public static Stream stream(String endpoint, Consumer stream) {
+ Objects.requireNonNull(endpoint, "endpoint must not be null");
+ Stream.Builder builder = Stream.builder().setEndpoint(endpoint);
+ Objects.requireNonNull(stream, "stream must not be null").accept(builder);
+ return builder.build();
+ }
+
+ /**
+ * Create a Voice Relay call destination
+ *
+ * @param endpoint URL of the server accepting the WebSocket request
+ * @param ttsVoice Default voice for speech synthesis
+ * @param sttLanguage BCP-47 language tag used to transcribe the inbound audio
+ * @return The Voice Relay destination
+ * @since 2.4
+ */
+ public static VoiceRelay voiceRelay(String endpoint, String ttsVoice, String sttLanguage) {
+ return voiceRelay(endpoint, ttsVoice, sttLanguage, voiceRelay -> {});
+ }
+
+ /**
+ * Create a Voice Relay call destination
+ *
+ * @param endpoint URL of the server accepting the WebSocket request
+ * @param ttsVoice Default voice for speech synthesis, from the catalog
+ * @param sttLanguage BCP-47 language tag used to transcribe the inbound audio
+ * @return The Voice Relay destination
+ * @since 2.4
+ */
+ public static VoiceRelay voiceRelay(String endpoint, VoiceName ttsVoice, String sttLanguage) {
+ return voiceRelay(endpoint, VoiceName.valueOf(ttsVoice), sttLanguage);
+ }
+
+ /**
+ * Create a Voice Relay call destination with optional fields, e.g. the interruptions or the call
+ * headers
+ *
+ * @param endpoint URL of the server accepting the WebSocket request
+ * @param ttsVoice Default voice for speech synthesis
+ * @param sttLanguage BCP-47 language tag used to transcribe the inbound audio
+ * @param voiceRelay Lambda setting the optional fields on the model builder, which already holds
+ * the required ones
+ * @return The Voice Relay destination
+ * @since 2.4
+ */
+ public static VoiceRelay voiceRelay(
+ String endpoint,
+ String ttsVoice,
+ String sttLanguage,
+ Consumer voiceRelay) {
+ Objects.requireNonNull(endpoint, "endpoint must not be null");
+ Objects.requireNonNull(ttsVoice, "ttsVoice must not be null");
+ Objects.requireNonNull(sttLanguage, "sttLanguage must not be null");
+ VoiceRelay.Builder builder =
+ VoiceRelay.builder()
+ .setEndpoint(endpoint)
+ .setTtsVoice(ttsVoice)
+ .setSttLanguage(sttLanguage);
+ Objects.requireNonNull(voiceRelay, "voiceRelay must not be null").accept(builder);
+ return builder.build();
+ }
+
+ /**
+ * Create a Voice Relay call destination with optional fields, e.g. the interruptions or the call
+ * headers
+ *
+ * @param endpoint URL of the server accepting the WebSocket request
+ * @param ttsVoice Default voice for speech synthesis, from the catalog
+ * @param sttLanguage BCP-47 language tag used to transcribe the inbound audio
+ * @param voiceRelay Lambda setting the optional fields on the model builder, which already holds
+ * the required ones
+ * @return The Voice Relay destination
+ * @since 2.4
+ */
+ public static VoiceRelay voiceRelay(
+ String endpoint,
+ VoiceName ttsVoice,
+ String sttLanguage,
+ Consumer voiceRelay) {
+ return voiceRelay(endpoint, VoiceName.valueOf(ttsVoice), sttLanguage, voiceRelay);
+ }
+
+ private static boolean isSip(String value) {
+ return value.startsWith(SIP_PREFIX) || value.startsWith(SIPS_PREFIX);
+ }
+
+ private static String withoutPhonePrefix(String value) {
+ return value.startsWith(PHONE_PREFIX) ? value.substring(PHONE_PREFIX.length()) : value;
+ }
+
+ /**
+ * {@link BaseDestination} around a generated destination model. Serialized as the wrapped model,
+ * so the JSON is the one of the {@link Phone}, {@link Sip} or {@link Stream} it holds.
+ */
+ static final class Endpoint implements BaseDestination {
+
+ private final Object destination;
+
+ Endpoint(Object destination) {
+ this.destination = destination;
+ }
+
+ @JsonValue
+ Object getDestination() {
+ return destination;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ return destination.equals(((Endpoint) o).destination);
+ }
+
+ @Override
+ public int hashCode() {
+ return destination.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return destination.toString();
+ }
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/DialCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/DialCreator.java
new file mode 100644
index 000000000..e5fec10c5
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/DialCreator.java
@@ -0,0 +1,370 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a dial command, returned by {@link Calls#dial(CallDestination)}
+ *
+ * Calling an event handler more than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class DialCreator {
+
+ private final CallDestination to;
+ private CallOrigin from;
+ private String name;
+ private Integer timeoutDurationSeconds;
+ private Integer maxDurationSeconds;
+ private List onAnswer;
+ private List onBusy;
+ private List onReject;
+ private List onTimeout;
+ private List onHangup;
+ private List onFailure;
+
+ DialCreator(CallDestination to) {
+ this.to = Objects.requireNonNull(to, "to must not be null");
+ }
+
+ /**
+ * Set the call origin
+ *
+ * @param from Call origin
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator from(CallOrigin from) {
+ this.from = Objects.requireNonNull(from, "from must not be null");
+ return this;
+ }
+
+ /**
+ * Set the call origin from a string, parsed with {@link Destination#of(String)}
+ *
+ * @param from Call origin, optionally prefixed with its type
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator from(String from) {
+ return from(Destination.of(from));
+ }
+
+ /**
+ * Set the name of the call leg, sent as callName
+ *
+ * @param name Name of the call leg, unique within the session
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator name(String name) {
+ this.name = Objects.requireNonNull(name, "name must not be null");
+ return this;
+ }
+
+ /**
+ * Set how long to wait for an answer, sent as dialTimeoutDurationSeconds
+ *
+ * @param seconds Timeout in seconds
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator timeoutDurationSeconds(int seconds) {
+ this.timeoutDurationSeconds = seconds;
+ return this;
+ }
+
+ /**
+ * Set the maximum duration of the call, sent as maxCallDurationSeconds
+ *
+ * @param seconds Maximum duration in seconds
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator maxDurationSeconds(int seconds) {
+ this.maxDurationSeconds = seconds;
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is answered
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onAnswer(SvamlCommand... commands) {
+ onAnswer = Commands.append(onAnswer, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is answered
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onAnswer(Collection extends SvamlCommand> commands) {
+ onAnswer = Commands.append(onAnswer, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is answered
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onAnswer(Consumer sequence) {
+ onAnswer = Commands.append(onAnswer, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the callee is busy
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onBusy(SvamlCommand... commands) {
+ onBusy = Commands.append(onBusy, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the callee is busy
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onBusy(Collection extends SvamlCommand> commands) {
+ onBusy = Commands.append(onBusy, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the callee is busy
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onBusy(Consumer sequence) {
+ onBusy = Commands.append(onBusy, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is rejected
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onReject(SvamlCommand... commands) {
+ onReject = Commands.append(onReject, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is rejected
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onReject(Collection extends SvamlCommand> commands) {
+ onReject = Commands.append(onReject, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is rejected
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onReject(Consumer sequence) {
+ onReject = Commands.append(onReject, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is not answered in time
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onTimeout(SvamlCommand... commands) {
+ onTimeout = Commands.append(onTimeout, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is not answered in time
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onTimeout(Collection extends SvamlCommand> commands) {
+ onTimeout = Commands.append(onTimeout, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is not answered in time
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onTimeout(Consumer sequence) {
+ onTimeout = Commands.append(onTimeout, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onHangup(SvamlCommand... commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onHangup(Collection extends SvamlCommand> commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onHangup(Consumer sequence) {
+ onHangup = Commands.append(onHangup, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call fails
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onFailure(SvamlCommand... commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call fails
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onFailure(Collection extends SvamlCommand> commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call fails
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public DialCreator onFailure(Consumer sequence) {
+ onFailure = Commands.append(onFailure, sequence);
+ return this;
+ }
+
+ /**
+ * Build the dial command
+ *
+ * @return The command. Its events are only set when a handler was given
+ * @since 2.4
+ */
+ public DialCommand build() {
+ DialCommand.Builder builder = DialCommand.builder().setTo(to);
+ if (null != from) {
+ builder.setFrom(from);
+ }
+ if (null != name) {
+ builder.setCallName(name);
+ }
+ if (null != timeoutDurationSeconds) {
+ builder.setDialTimeoutDurationSeconds(timeoutDurationSeconds);
+ }
+ if (null != maxDurationSeconds) {
+ builder.setMaxCallDurationSeconds(maxDurationSeconds);
+ }
+ if (hasEvents()) {
+ builder.setEvents(buildEvents());
+ }
+ return builder.build();
+ }
+
+ private boolean hasEvents() {
+ return null != onAnswer
+ || null != onBusy
+ || null != onReject
+ || null != onTimeout
+ || null != onHangup
+ || null != onFailure;
+ }
+
+ private CallEvents buildEvents() {
+ CallEvents.Builder builder = CallEvents.builder();
+ if (null != onAnswer) {
+ builder.setOnAnswer(Commands.copy(onAnswer));
+ }
+ if (null != onBusy) {
+ builder.setOnBusy(Commands.copy(onBusy));
+ }
+ if (null != onReject) {
+ builder.setOnReject(Commands.copy(onReject));
+ }
+ if (null != onTimeout) {
+ builder.setOnTimeout(Commands.copy(onTimeout));
+ }
+ if (null != onHangup) {
+ builder.setOnHangup(Commands.copy(onHangup));
+ }
+ if (null != onFailure) {
+ builder.setOnFailure(Commands.copy(onFailure));
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/IncomingCallResponseCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/IncomingCallResponseCreator.java
new file mode 100644
index 000000000..445fee3f3
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/IncomingCallResponseCreator.java
@@ -0,0 +1,101 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse;
+import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of the response to a call.incoming Sinch Event
+ *
+ * Returned by {@link SinchEventResponses#incomingCallResponse(SvamlCommand...)}.
+ *
+ *
Calling {@link #onHangup} more than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class IncomingCallResponseCreator {
+
+ private final List commands;
+ private String name;
+ private List onHangup;
+
+ IncomingCallResponseCreator(List commands) {
+ this.commands = commands;
+ }
+
+ /**
+ * Set the name of the incoming call, sent as callName
+ *
+ * @param name Name of the call, 1 to 32 characters without spaces
+ * @return This creator
+ * @since 2.4
+ */
+ public IncomingCallResponseCreator name(String name) {
+ this.name = Objects.requireNonNull(name, "name must not be null");
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public IncomingCallResponseCreator onHangup(SvamlCommand... commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public IncomingCallResponseCreator onHangup(Collection extends SvamlCommand> commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public IncomingCallResponseCreator onHangup(Consumer sequence) {
+ onHangup = Commands.append(onHangup, sequence);
+ return this;
+ }
+
+ /**
+ * Build the response
+ *
+ * @return The response, to serialize with the Sinch Events service
+ * @throws IllegalStateException if a gotoMenu command is outside a menu, or if a
+ * menu has a start menu or a gotoMenu target that is not one of its items
+ * @since 2.4
+ */
+ public VoiceSinchEventResponse build() {
+ MenuCreator.checkGotoTargets(commands, Collections.emptySet());
+ MenuCreator.checkGotoTargets(onHangup, Collections.emptySet());
+ VoiceSinchEventResponse.Builder builder =
+ VoiceSinchEventResponse.builder().setCommands(Commands.copy(commands));
+ if (null != name) {
+ builder.setCallName(name);
+ }
+ if (null != onHangup) {
+ builder.setEvents(
+ IncomingCallResponseEvents.builder().setOnHangup(Commands.copy(onHangup)).build());
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Menu.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Menu.java
new file mode 100644
index 000000000..b5edda833
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Menu.java
@@ -0,0 +1,84 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.GotoMenuCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuItem;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuPrompt;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Helpers to build the interactive menu SVAML commands
+ *
+ * @since 2.4
+ */
+public final class Menu {
+
+ private Menu() {}
+
+ /**
+ * Run an interactive menu that collects user input (e.g. DTMF digits)
+ *
+ * The sequence waits for the menu to end before running the next command.
+ *
+ * @param startMenu Name of the item played first, 1 to 16 characters. It must be one of the items
+ * of the menu
+ * @return The creator of the menu command
+ * @since 2.4
+ */
+ public static MenuCreator create(String startMenu) {
+ return new MenuCreator(startMenu);
+ }
+
+ /**
+ * Jump to another item of the enclosing menu
+ *
+ *
Only valid within a menu: in the match or onFailure commands of an
+ * item, including their nested event handlers. The target must be an item of that menu.
+ *
+ *
The target is checked when one of these is built: a menu ({@link MenuCreator}), a sequence
+ * ({@link CommandsSequenceCreator}), a static call behavior or a response to an incoming call.
+ * Building another creator alone, e.g. {@link DialCreator}, does not check it.
+ *
+ *
Named after the API command: goto is a reserved word in Java.
+ *
+ * @param name Name of the menu item, 1 to 16 characters, sent as menuName
+ * @return The gotoMenu command
+ * @since 2.4
+ */
+ public static GotoMenuCommand gotoMenu(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ return GotoMenuCommand.builder().setMenuName(name).build();
+ }
+
+ /**
+ * Create a menu prompt, e.g. to share it between several items or as repeat prompt
+ *
+ * @param prompt Lambda filling the prompt
+ * @return The menu prompt
+ * @throws IllegalStateException if the lambda adds no message
+ * @since 2.4
+ */
+ public static MenuPrompt prompt(Consumer prompt) {
+ Objects.requireNonNull(prompt, "prompt must not be null");
+ PromptCreator creator = new PromptCreator();
+ prompt.accept(creator);
+ return creator.build();
+ }
+
+ /**
+ * Create a menu item, e.g. to share it between several menus
+ *
+ * Add it to a menu with {@link MenuCreator#item(String, MenuItem)}. Its gotoMenu
+ * targets are checked when that menu is built.
+ *
+ * @param item Lambda filling the item
+ * @return The menu item
+ * @since 2.4
+ */
+ public static MenuItem item(Consumer item) {
+ Objects.requireNonNull(item, "item must not be null");
+ MenuItemCreator creator = new MenuItemCreator();
+ item.accept(creator);
+ return creator.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuCreator.java
new file mode 100644
index 000000000..9afd7023a
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuCreator.java
@@ -0,0 +1,163 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.amd.AmdCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.amd.AmdEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.GotoMenuCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuItem;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.StartRecordingCommand;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a menu command, returned by {@link Menu#create(String)}
+ *
+ * {@link #build()} checks that the start menu and every {@link Menu#gotoMenu(String)} target are
+ * items of this menu.
+ *
+ * @since 2.4
+ */
+public final class MenuCreator {
+
+ private final String startMenu;
+ private final Map items = new LinkedHashMap<>();
+
+ MenuCreator(String startMenu) {
+ this.startMenu = Objects.requireNonNull(startMenu, "startMenu must not be null");
+ }
+
+ /**
+ * Add a menu item
+ *
+ * @param name Name of the item, 1 to 16 characters. Start menu and gotoMenu targets refer to it
+ * @param item Lambda filling the item
+ * @return This creator
+ * @throws IllegalArgumentException if an item with this name is already defined
+ * @since 2.4
+ */
+ public MenuCreator item(String name, Consumer item) {
+ checkNewItem(name);
+ return item(name, Menu.item(item));
+ }
+
+ /**
+ * Add a menu item
+ *
+ * @param name Name of the item, 1 to 16 characters. Start menu and gotoMenu targets refer to it
+ * @param item Menu item, e.g. built with {@link Menu#item(Consumer)} to share it
+ * @return This creator
+ * @throws IllegalArgumentException if an item with this name is already defined
+ * @since 2.4
+ */
+ public MenuCreator item(String name, MenuItem item) {
+ checkNewItem(name);
+ items.put(name, Objects.requireNonNull(item, "item must not be null"));
+ return this;
+ }
+
+ /**
+ * Build the menu command
+ *
+ * @return The command
+ * @throws IllegalStateException if the start menu, or the target of a gotoMenu
+ * command of an item, is not an item of this menu
+ * @since 2.4
+ */
+ public MenuCommand build() {
+ checkMenu(startMenu, items);
+ return MenuCommand.builder()
+ .setStartMenu(startMenu)
+ .setMenus(new LinkedHashMap<>(items))
+ .build();
+ }
+
+ private void checkNewItem(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ if (items.containsKey(name)) {
+ throw new IllegalArgumentException(
+ String.format("menu item \"%s\" is already defined", name));
+ }
+ }
+
+ /** The start menu, and every gotoMenu target of the items, must be an item */
+ static void checkMenu(String startMenu, Map menus) {
+ Map items = null == menus ? Collections.emptyMap() : menus;
+ if (!items.containsKey(startMenu)) {
+ throw new IllegalStateException(String.format("menu start \"%s\" is not defined", startMenu));
+ }
+ for (MenuItem item : items.values()) {
+ if (null != item.getMatches()) {
+ for (List commands : item.getMatches().values()) {
+ checkGotoTargets(commands, items.keySet());
+ }
+ }
+ checkGotoTargets(item.getOnFailure(), items.keySet());
+ }
+ }
+
+ /**
+ * A gotoMenu target must be an item of the enclosing menu. Nested event handlers are
+ * searched too. A nested menu is checked against its own items, so one built without
+ * this creator (e.g. with {@link MenuCommand#builder()}) is checked as well. With no names, the
+ * commands are outside any menu, where gotoMenu is not valid.
+ */
+ static void checkGotoTargets(List commands, Set names) {
+ if (null == commands) {
+ return;
+ }
+ for (SvamlCommand command : commands) {
+ if (command instanceof GotoMenuCommand) {
+ String target = ((GotoMenuCommand) command).getMenuName();
+ if (names.isEmpty()) {
+ throw new IllegalStateException(
+ String.format("gotoMenu \"%s\" is only valid within a menu", target));
+ }
+ if (!names.contains(target)) {
+ throw new IllegalStateException(String.format("gotoMenu \"%s\" is not defined", target));
+ }
+ } else if (command instanceof MenuCommand) {
+ MenuCommand menu = (MenuCommand) command;
+ checkMenu(menu.getStartMenu(), menu.getMenus());
+ } else if (command instanceof DialCommand) {
+ CallEvents events = ((DialCommand) command).getEvents();
+ if (null != events) {
+ checkGotoTargets(events.getOnAnswer(), names);
+ checkGotoTargets(events.getOnBusy(), names);
+ checkGotoTargets(events.getOnReject(), names);
+ checkGotoTargets(events.getOnTimeout(), names);
+ checkGotoTargets(events.getOnHangup(), names);
+ checkGotoTargets(events.getOnFailure(), names);
+ }
+ } else if (command instanceof AmdCommand) {
+ AmdEvents events = ((AmdCommand) command).getEvents();
+ if (null != events) {
+ checkGotoTargets(events.getOnHuman(), names);
+ checkGotoTargets(events.getOnMachine(), names);
+ checkGotoTargets(events.getOnBeep(), names);
+ checkGotoTargets(events.getOnUnknown(), names);
+ }
+ } else if (command instanceof MessagesCommand) {
+ MessagesCommand messages = (MessagesCommand) command;
+ if (null != messages.getEvents()) {
+ checkGotoTargets(messages.getEvents().getOnFinish(), names);
+ }
+ } else if (command instanceof StartRecordingCommand) {
+ RecordingEvents events = ((StartRecordingCommand) command).getEvents();
+ if (null != events) {
+ checkGotoTargets(events.getOnFinish(), names);
+ checkGotoTargets(events.getOnFailure(), names);
+ }
+ }
+ }
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuItemCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuItemCreator.java
new file mode 100644
index 000000000..a41259af5
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MenuItemCreator.java
@@ -0,0 +1,293 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuItem;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuPrompt;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a menu item, given to the lambdas of {@link MenuCreator#item(String, Consumer)}
+ * and {@link Menu#item(Consumer)}
+ *
+ * Calling {@link #match} more than once for the same input, or {@link #onFailure} more than
+ * once, appends the commands.
+ *
+ * @since 2.4
+ */
+public final class MenuItemCreator {
+
+ private MenuPrompt prompt;
+ private MenuPrompt repeatPrompt;
+ private Integer inputTimeoutDurationSeconds;
+ private Integer repeatCount;
+ private Integer minimumInputLength;
+ private Integer maximumInputLength;
+ private String terminatingSequence;
+ private List inputMethods;
+ private final Map> matches = new LinkedHashMap<>();
+ private List onFailure;
+
+ MenuItemCreator() {}
+
+ /**
+ * Set the prompt played when the menu starts. It is also the repeat prompt when none is set
+ *
+ * @param prompt Lambda filling the prompt
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator prompt(Consumer prompt) {
+ return prompt(Menu.prompt(prompt));
+ }
+
+ /**
+ * Set the prompt played when the menu starts. It is also the repeat prompt when none is set
+ *
+ * @param prompt Prompt, e.g. built with {@link Menu#prompt(Consumer)} to share it
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator prompt(MenuPrompt prompt) {
+ this.prompt = Objects.requireNonNull(prompt, "prompt must not be null");
+ return this;
+ }
+
+ /**
+ * Set the prompt played when the menu is repeated, after a timeout or an input that does not
+ * match. The API only accepts it when a prompt is also set
+ *
+ * @param repeatPrompt Lambda filling the prompt
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator repeatPrompt(Consumer repeatPrompt) {
+ return repeatPrompt(Menu.prompt(repeatPrompt));
+ }
+
+ /**
+ * Set the prompt played when the menu is repeated, after a timeout or an input that does not
+ * match. The API only accepts it when a prompt is also set
+ *
+ * @param repeatPrompt Prompt, e.g. built with {@link Menu#prompt(Consumer)} to share it
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator repeatPrompt(MenuPrompt repeatPrompt) {
+ this.repeatPrompt = Objects.requireNonNull(repeatPrompt, "repeatPrompt must not be null");
+ return this;
+ }
+
+ /**
+ * Set how long to wait for input before the attempt times out
+ *
+ * @param seconds Timeout in seconds, from 1 to 60. The API default is 5
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator inputTimeoutDurationSeconds(int seconds) {
+ this.inputTimeoutDurationSeconds = seconds;
+ return this;
+ }
+
+ /**
+ * Set how many times the menu is repeated, after a timeout or an input that does not match
+ *
+ * @param repeatCount Maximum number of repetitions, from 0 to 10. The API default is 2
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator repeatCount(int repeatCount) {
+ this.repeatCount = repeatCount;
+ return this;
+ }
+
+ /**
+ * Set the number of input characters required before the input is evaluated
+ *
+ * @param length Minimum input length, from 1 to 100. The API default is 1
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator minimumInputLength(int length) {
+ this.minimumInputLength = length;
+ return this;
+ }
+
+ /**
+ * Set the number of input characters that triggers the evaluation of the input
+ *
+ * @param length Maximum input length, from 1 to 100. The API default is 1
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator maximumInputLength(int length) {
+ this.maximumInputLength = length;
+ return this;
+ }
+
+ /**
+ * Set the characters that end the input and trigger its evaluation, e.g. #. They are
+ * included in the evaluated input
+ *
+ * @param terminatingSequence Terminating sequence: digits, * and #, at
+ * most 10 characters
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator terminatingSequence(String terminatingSequence) {
+ this.terminatingSequence =
+ Objects.requireNonNull(terminatingSequence, "terminatingSequence must not be null");
+ return this;
+ }
+
+ /**
+ * Set the accepted input methods. The API default is DTMF
+ *
+ * @param inputMethods Input methods
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator inputMethods(MenuItem.InputMethodsEnum... inputMethods) {
+ Objects.requireNonNull(inputMethods, "inputMethods must not be null");
+ List list = new ArrayList<>();
+ for (MenuItem.InputMethodsEnum inputMethod : Arrays.asList(inputMethods)) {
+ list.add(Objects.requireNonNull(inputMethod, "inputMethod must not be null"));
+ }
+ this.inputMethods = list;
+ return this;
+ }
+
+ /**
+ * Add commands to run when the input matches
+ *
+ * The inputs are evaluated in the order they are first added: the first one that matches runs
+ * its commands. At most 50 inputs per item.
+ *
+ *
The input can be a regular expression, e.g. [2-4], so the star tone must be
+ * escaped: write "\\*" to match it.
+ *
+ * @param input Literal string or regular expression, 1 to 256 characters
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator match(String input, SvamlCommand... commands) {
+ Objects.requireNonNull(input, "input must not be null");
+ matches.put(input, Commands.append(matches.get(input), commands));
+ return this;
+ }
+
+ /**
+ * Add commands to run when the input matches
+ *
+ *
The inputs are evaluated in the order they are first added: the first one that matches runs
+ * its commands. At most 50 inputs per item.
+ *
+ *
The input can be a regular expression, e.g. [2-4], so the star tone must be
+ * escaped: write "\\*" to match it.
+ *
+ * @param input Literal string or regular expression, 1 to 256 characters
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator match(String input, Collection extends SvamlCommand> commands) {
+ Objects.requireNonNull(input, "input must not be null");
+ matches.put(input, Commands.append(matches.get(input), commands));
+ return this;
+ }
+
+ /**
+ * Add commands to run when the input matches
+ *
+ * @param input Literal string or regular expression, see {@link #match(String, SvamlCommand...)}
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator match(String input, Consumer sequence) {
+ Objects.requireNonNull(input, "input must not be null");
+ matches.put(input, Commands.append(matches.get(input), sequence));
+ return this;
+ }
+
+ /**
+ * Add commands to run when no input matches once the repeat limit is reached
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator onFailure(SvamlCommand... commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when no input matches once the repeat limit is reached
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator onFailure(Collection extends SvamlCommand> commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when no input matches once the repeat limit is reached
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MenuItemCreator onFailure(Consumer sequence) {
+ onFailure = Commands.append(onFailure, sequence);
+ return this;
+ }
+
+ MenuItem build() {
+ MenuItem.Builder builder = MenuItem.builder();
+ if (null != prompt) {
+ builder.setPrompt(prompt);
+ }
+ if (null != repeatPrompt) {
+ builder.setRepeatPrompt(repeatPrompt);
+ }
+ if (null != inputTimeoutDurationSeconds) {
+ builder.setInputTimeoutDurationSeconds(inputTimeoutDurationSeconds);
+ }
+ if (null != repeatCount) {
+ builder.setRepeatCount(repeatCount);
+ }
+ if (null != minimumInputLength) {
+ builder.setMinimumInputLength(minimumInputLength);
+ }
+ if (null != maximumInputLength) {
+ builder.setMaximumInputLength(maximumInputLength);
+ }
+ if (null != terminatingSequence) {
+ builder.setTerminatingSequence(terminatingSequence);
+ }
+ if (null != inputMethods) {
+ builder.setInputMethods(new ArrayList<>(inputMethods));
+ }
+ if (!matches.isEmpty()) {
+ Map> copy = new LinkedHashMap<>();
+ matches.forEach((input, commands) -> copy.put(input, Commands.copy(commands)));
+ builder.setMatches(copy);
+ }
+ if (null != onFailure) {
+ builder.setOnFailure(Commands.copy(onFailure));
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Messages.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Messages.java
new file mode 100644
index 000000000..d4bc745ee
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Messages.java
@@ -0,0 +1,193 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.PlayMessage;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.StopMessagesCommand;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Helpers to build the message playback SVAML commands
+ *
+ * {@link #text}, {@link #ssml} and {@link #play} are shortcuts for a messages
+ * command with a single message. Their lambda overloads also set a name or the commands to run once
+ * played. Use {@link #start()} to play several messages.
+ *
+ * @since 2.4
+ */
+public final class Messages {
+
+ private Messages() {}
+
+ /**
+ * Play a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return The messages command, with this single message
+ * @since 2.4
+ */
+ public static MessagesCommand text(String text, String voiceName) {
+ return start().text(text, voiceName).build();
+ }
+
+ /**
+ * Play a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return The messages command, with this single message
+ * @since 2.4
+ */
+ public static MessagesCommand text(String text, VoiceName voiceName) {
+ return text(text, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Play a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return The messages command, with this single message
+ * @since 2.4
+ */
+ public static MessagesCommand ssml(String ssml, String voiceName) {
+ return start().ssml(ssml, voiceName).build();
+ }
+
+ /**
+ * Play a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return The messages command, with this single message
+ * @since 2.4
+ */
+ public static MessagesCommand ssml(String ssml, VoiceName voiceName) {
+ return ssml(ssml, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Play an audio file
+ *
+ * @param url URL of the media to play
+ * @return The messages command, with this single message
+ * @since 2.4
+ */
+ public static MessagesCommand play(String url) {
+ return start().play(url).build();
+ }
+
+ /**
+ * Play a text-to-speech message in plain text, with a name or commands run once it is played
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return The messages command
+ * @since 2.4
+ */
+ public static MessagesCommand text(
+ String text, String voiceName, Consumer messages) {
+ return fill(start().text(text, voiceName), messages);
+ }
+
+ /**
+ * Play a text-to-speech message in plain text, with a name or commands run once it is played
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return The messages command
+ * @since 2.4
+ */
+ public static MessagesCommand text(
+ String text, VoiceName voiceName, Consumer messages) {
+ return text(text, VoiceName.valueOf(voiceName), messages);
+ }
+
+ /**
+ * Play a text-to-speech message in SSML, with a name or commands run once it is played
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return The messages command
+ * @since 2.4
+ */
+ public static MessagesCommand ssml(
+ String ssml, String voiceName, Consumer messages) {
+ return fill(start().ssml(ssml, voiceName), messages);
+ }
+
+ /**
+ * Play a text-to-speech message in SSML, with a name or commands run once it is played
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return The messages command
+ * @since 2.4
+ */
+ public static MessagesCommand ssml(
+ String ssml, VoiceName voiceName, Consumer messages) {
+ return ssml(ssml, VoiceName.valueOf(voiceName), messages);
+ }
+
+ /**
+ * Play an audio file, with a name or commands run once it is played
+ *
+ * @param url URL of the media to play
+ * @param messages Lambda setting the name, the commands run once played, or more messages
+ * @return The messages command
+ * @since 2.4
+ */
+ public static MessagesCommand play(String url, Consumer messages) {
+ return fill(start().play(url), messages);
+ }
+
+ private static MessagesCommand fill(MessagesCreator creator, Consumer messages) {
+ Objects.requireNonNull(messages, "messages must not be null").accept(creator);
+ return creator.build();
+ }
+
+ /**
+ * Play one or more messages, in the order they are added
+ *
+ * @return The creator of the messages command
+ * @since 2.4
+ */
+ public static MessagesCreator start() {
+ return new MessagesCreator();
+ }
+
+ /**
+ * Stop a named message sequence
+ *
+ * @param name Name of the message sequence, sent as messagesName
+ * @return The stopMessages command
+ * @since 2.4
+ */
+ public static StopMessagesCommand stop(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ return StopMessagesCommand.builder().setMessagesName(name).build();
+ }
+
+ /** format is only set for SSML: plain text is the API default */
+ static SayMessage sayMessage(String text, String voiceName, SayMessage.FormatEnum format) {
+ Objects.requireNonNull(text, "text must not be null");
+ Objects.requireNonNull(voiceName, "voiceName must not be null");
+ SayMessage.Builder builder = SayMessage.builder().setText(text).setVoiceName(voiceName);
+ if (null != format) {
+ builder.setFormat(format);
+ }
+ return builder.build();
+ }
+
+ static PlayMessage playMessage(String url) {
+ Objects.requireNonNull(url, "url must not be null");
+ return PlayMessage.builder().setUrl(url).build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesCreator.java
new file mode 100644
index 000000000..9e07e3375
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesCreator.java
@@ -0,0 +1,163 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.Message;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a messages command, returned by {@link Messages#start()}
+ *
+ * Messages are played in the order they are added, at most 10. Calling {@link #onFinish} more
+ * than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class MessagesCreator {
+
+ private final List messages = new ArrayList<>();
+ private String name;
+ private List onFinish;
+
+ MessagesCreator() {}
+
+ /**
+ * Add a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator text(String text, String voiceName) {
+ messages.add(Messages.sayMessage(text, voiceName, null));
+ return this;
+ }
+
+ /**
+ * Add a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator text(String text, VoiceName voiceName) {
+ return text(text, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator ssml(String ssml, String voiceName) {
+ messages.add(Messages.sayMessage(ssml, voiceName, SayMessage.FormatEnum.SSML));
+ return this;
+ }
+
+ /**
+ * Add a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator ssml(String ssml, VoiceName voiceName) {
+ return ssml(ssml, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add an audio file to play
+ *
+ * @param url URL of the media to play
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator play(String url) {
+ messages.add(Messages.playMessage(url));
+ return this;
+ }
+
+ /**
+ * Set the name of the message sequence, sent as messagesName. A named sequence can
+ * be stopped with {@link Messages#stop(String)}
+ *
+ * @param name Name of the message sequence, unique within the session
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator name(String name) {
+ this.name = Objects.requireNonNull(name, "name must not be null");
+ return this;
+ }
+
+ /**
+ * Add commands to run when all the messages have been played
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator onFinish(SvamlCommand... commands) {
+ onFinish = Commands.append(onFinish, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when all the messages have been played
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator onFinish(Collection extends SvamlCommand> commands) {
+ onFinish = Commands.append(onFinish, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when all the messages have been played
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public MessagesCreator onFinish(Consumer sequence) {
+ onFinish = Commands.append(onFinish, sequence);
+ return this;
+ }
+
+ /**
+ * Build the messages command
+ *
+ * @return The command
+ * @throws IllegalStateException if no message was added
+ * @since 2.4
+ */
+ public MessagesCommand build() {
+ if (messages.isEmpty()) {
+ throw new IllegalStateException("messages requires at least one text, ssml or play message");
+ }
+ MessagesCommand.Builder builder =
+ MessagesCommand.builder().setMessages(new ArrayList<>(messages));
+ if (null != name) {
+ builder.setMessagesName(name);
+ }
+ if (null != onFinish) {
+ builder.setEvents(MessageEvents.builder().setOnFinish(Commands.copy(onFinish)).build());
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/PromptCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/PromptCreator.java
new file mode 100644
index 000000000..620f7a097
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/PromptCreator.java
@@ -0,0 +1,110 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuPrompt;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.Message;
+import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fluent creator of a menu prompt, given to the lambdas of {@link MenuItemCreator#prompt} and
+ * {@link Menu#prompt}
+ *
+ * Messages are played in the order they are added, at most 10.
+ *
+ * @since 2.4
+ */
+public final class PromptCreator {
+
+ private final List messages = new ArrayList<>();
+ private Boolean allowBargeIn;
+
+ PromptCreator() {}
+
+ /**
+ * Add a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator text(String text, String voiceName) {
+ messages.add(Messages.sayMessage(text, voiceName, null));
+ return this;
+ }
+
+ /**
+ * Add a text-to-speech message in plain text
+ *
+ * @param text Text to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator text(String text, VoiceName voiceName) {
+ return text(text, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator ssml(String ssml, String voiceName) {
+ messages.add(Messages.sayMessage(ssml, voiceName, SayMessage.FormatEnum.SSML));
+ return this;
+ }
+
+ /**
+ * Add a text-to-speech message in SSML
+ *
+ * @param ssml SSML document (<speak>...</speak>) to synthesize
+ * @param voiceName Voice used for speech synthesis, from the catalog
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator ssml(String ssml, VoiceName voiceName) {
+ return ssml(ssml, VoiceName.valueOf(voiceName));
+ }
+
+ /**
+ * Add an audio file to play
+ *
+ * @param url URL of the media to play
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator play(String url) {
+ messages.add(Messages.playMessage(url));
+ return this;
+ }
+
+ /**
+ * Set whether user input interrupts the prompt. The API default is true
+ *
+ * @param allowBargeIn false to play the whole prompt before evaluating the input,
+ * which is still collected during playback
+ * @return This creator
+ * @since 2.4
+ */
+ public PromptCreator allowBargeIn(boolean allowBargeIn) {
+ this.allowBargeIn = allowBargeIn;
+ return this;
+ }
+
+ MenuPrompt build() {
+ if (messages.isEmpty()) {
+ throw new IllegalStateException("prompt requires at least one text, ssml or play message");
+ }
+ MenuPrompt.Builder builder = MenuPrompt.builder().setMessages(new ArrayList<>(messages));
+ if (null != allowBargeIn) {
+ builder.setAllowBargeIn(allowBargeIn);
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Recording.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Recording.java
new file mode 100644
index 000000000..c3f173eaf
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/Recording.java
@@ -0,0 +1,108 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingOptions;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.StopRecordingCommand;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Helpers to build the call recording SVAML commands
+ *
+ * @since 2.4
+ */
+public final class Recording {
+
+ private Recording() {}
+
+ /**
+ * Create the options to store a recording in a third party storage
+ *
+ * Only the required fields are set. To set the format, the recording type or the
+ * transcription, use {@link #options(RecordingDestinationType, String, String, Consumer)}, or the
+ * creator returned by {@link #start(RecordingDestinationType, String, String)}.
+ *
+ * @param destination Third party storage the recording is delivered to
+ * @param url Destination URL, sent as destinationUrl. Its scheme depends on the
+ * destination: s3, gs or azure
+ * @param credentials Credentials to the third party storage
+ * @return The recording options
+ * @since 2.4
+ */
+ public static RecordingOptions options(
+ RecordingDestinationType destination, String url, String credentials) {
+ return options(destination, url, credentials, options -> {});
+ }
+
+ /**
+ * Create the options to store a recording in a third party storage, with optional fields, e.g. to
+ * share them between several recordings
+ *
+ * @param destination Third party storage the recording is delivered to
+ * @param url Destination URL, sent as destinationUrl. Its scheme depends on the
+ * destination: s3, gs or azure
+ * @param credentials Credentials to the third party storage
+ * @param options Lambda setting the optional fields (format, recording type, transcription) on
+ * the model builder, which already holds the required ones
+ * @return The recording options
+ * @since 2.4
+ */
+ public static RecordingOptions options(
+ RecordingDestinationType destination,
+ String url,
+ String credentials,
+ Consumer options) {
+ Objects.requireNonNull(destination, "destination must not be null");
+ Objects.requireNonNull(url, "url must not be null");
+ Objects.requireNonNull(credentials, "credentials must not be null");
+ RecordingOptions.Builder builder =
+ RecordingOptions.builder()
+ .setDestination(destination)
+ .setDestinationUrl(url)
+ .setCredentials(credentials);
+ Objects.requireNonNull(options, "options must not be null").accept(builder);
+ return builder.build();
+ }
+
+ /**
+ * Start recording the call, stored in a third party storage
+ *
+ * Set the optional recording options (format, recording type, transcription) on the returned
+ * creator.
+ *
+ * @param destination Third party storage the recording is delivered to
+ * @param url Destination URL, sent as destinationUrl. Its scheme depends on the
+ * destination: s3, gs or azure
+ * @param credentials Credentials to the third party storage
+ * @return The creator of the startRecording command
+ * @since 2.4
+ */
+ public static RecordingCreator start(
+ RecordingDestinationType destination, String url, String credentials) {
+ return new RecordingCreator(destination, url, credentials);
+ }
+
+ /**
+ * Start recording the call with existing recording options, e.g. shared by several recordings
+ *
+ * @param options Recording options, see {@link #options(RecordingDestinationType, String,
+ * String)}
+ * @return The creator of the startRecording command
+ * @since 2.4
+ */
+ public static RecordingCreator start(RecordingOptions options) {
+ return new RecordingCreator(options);
+ }
+
+ /**
+ * Stop a named recording
+ *
+ * @param name Name of the recording, sent as recordingName
+ * @return The stopRecording command
+ * @since 2.4
+ */
+ public static StopRecordingCommand stop(String name) {
+ Objects.requireNonNull(name, "name must not be null");
+ return StopRecordingCommand.builder().setRecordingName(name).build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingCreator.java
new file mode 100644
index 000000000..a7d3fcdf8
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingCreator.java
@@ -0,0 +1,236 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingFormatType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingOptions;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.StartRecordingCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.TranscriptionOptions;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a startRecording command
+ *
+ *
Returned by {@link Recording#start(RecordingDestinationType, String, String)}.
+ *
+ *
Also returned by {@link Recording#start(RecordingOptions)}: the optional recording options set
+ * here override the ones of the given {@link RecordingOptions}.
+ *
+ *
Calling an event handler more than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class RecordingCreator {
+
+ // Options given as is: sent unchanged unless an optional field is overridden
+ private final RecordingOptions options;
+ private boolean overridden;
+ private final RecordingDestinationType destination;
+ private final String url;
+ private final String credentials;
+ private RecordingFormatType format;
+ private RecordingType recordingType;
+ private TranscriptionOptions transcriptionOptions;
+ private String name;
+ private List onFinish;
+ private List onFailure;
+
+ RecordingCreator(RecordingDestinationType destination, String url, String credentials) {
+ this.options = null;
+ this.destination = Objects.requireNonNull(destination, "destination must not be null");
+ this.url = Objects.requireNonNull(url, "url must not be null");
+ this.credentials = Objects.requireNonNull(credentials, "credentials must not be null");
+ }
+
+ RecordingCreator(RecordingOptions options) {
+ this.options = Objects.requireNonNull(options, "options must not be null");
+ this.destination = options.getDestination();
+ this.url = options.getDestinationUrl();
+ this.credentials = options.getCredentials();
+ this.format = options.getFormat();
+ this.recordingType = options.getRecordingType();
+ this.transcriptionOptions = options.getTranscriptionOptions();
+ }
+
+ /**
+ * Set the audio format of the recording. The API default is MP3
+ *
+ * @param format Audio format
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator format(RecordingFormatType format) {
+ this.format = Objects.requireNonNull(format, "format must not be null");
+ overridden = true;
+ return this;
+ }
+
+ /**
+ * Set which voice streams are recorded. The API default is COMBINED
+ *
+ * @param recordingType Recorded voice streams
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator recordingType(RecordingType recordingType) {
+ this.recordingType = Objects.requireNonNull(recordingType, "recordingType must not be null");
+ overridden = true;
+ return this;
+ }
+
+ /**
+ * Set the speech-to-text transcription of the recording
+ *
+ * @param transcriptionOptions Transcription options
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator transcriptionOptions(TranscriptionOptions transcriptionOptions) {
+ this.transcriptionOptions =
+ Objects.requireNonNull(transcriptionOptions, "transcriptionOptions must not be null");
+ overridden = true;
+ return this;
+ }
+
+ /**
+ * Set the name of the recording, sent as recordingName. Without a name, the
+ * recording can only be stopped by ending the call
+ *
+ * @param name Name of the recording, unique within the session
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator name(String name) {
+ this.name = Objects.requireNonNull(name, "name must not be null");
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording is stopped
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFinish(SvamlCommand... commands) {
+ onFinish = Commands.append(onFinish, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording is stopped
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFinish(Collection extends SvamlCommand> commands) {
+ onFinish = Commands.append(onFinish, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording is stopped
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFinish(Consumer sequence) {
+ onFinish = Commands.append(onFinish, sequence);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording fails to start
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFailure(SvamlCommand... commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording fails to start
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFailure(Collection extends SvamlCommand> commands) {
+ onFailure = Commands.append(onFailure, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the recording fails to start
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public RecordingCreator onFailure(Consumer sequence) {
+ onFailure = Commands.append(onFailure, sequence);
+ return this;
+ }
+
+ /**
+ * Build the startRecording command
+ *
+ * @return The command. Its events are only set when a handler was given
+ * @since 2.4
+ */
+ public StartRecordingCommand build() {
+ StartRecordingCommand.Builder builder =
+ StartRecordingCommand.builder().setRecordingOptions(buildOptions());
+ if (null != name) {
+ builder.setRecordingName(name);
+ }
+ if (null != onFinish || null != onFailure) {
+ RecordingEvents.Builder events = RecordingEvents.builder();
+ if (null != onFinish) {
+ events.setOnFinish(Commands.copy(onFinish));
+ }
+ if (null != onFailure) {
+ events.setOnFailure(Commands.copy(onFailure));
+ }
+ builder.setEvents(events.build());
+ }
+ return builder.build();
+ }
+
+ /** Only the fields that have a value are set, so unset ones are left to the API defaults */
+ private RecordingOptions buildOptions() {
+ if (null != options && !overridden) {
+ return options;
+ }
+ RecordingOptions.Builder builder = RecordingOptions.builder();
+ if (null != destination) {
+ builder.setDestination(destination);
+ }
+ if (null != url) {
+ builder.setDestinationUrl(url);
+ }
+ if (null != credentials) {
+ builder.setCredentials(credentials);
+ }
+ if (null != format) {
+ builder.setFormat(format);
+ }
+ if (null != recordingType) {
+ builder.setRecordingType(recordingType);
+ }
+ if (null != transcriptionOptions) {
+ builder.setTranscriptionOptions(transcriptionOptions);
+ }
+ return builder.build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponses.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponses.java
new file mode 100644
index 000000000..748b1d320
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponses.java
@@ -0,0 +1,101 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Helpers to build the response returned to a Voice V2 Sinch Event
+ *
+ * Serialize the response with the serializeResponse method of the Sinch Events
+ * service.
+ *
+ * @since 2.4
+ */
+public final class SinchEventResponses {
+
+ private SinchEventResponses() {}
+
+ /**
+ * Build the response to a call.incoming event, which can also name the call and set
+ * the commands to run when it is hung up
+ *
+ * @param commands Commands to run
+ * @return The creator of the response
+ * @since 2.4
+ */
+ public static IncomingCallResponseCreator incomingCallResponse(SvamlCommand... commands) {
+ return new IncomingCallResponseCreator(Commands.append(null, commands));
+ }
+
+ /**
+ * Build the response to a call.incoming event
+ *
+ * @param commands Commands to run
+ * @return The creator of the response
+ * @see #incomingCallResponse(SvamlCommand...)
+ * @since 2.4
+ */
+ public static IncomingCallResponseCreator incomingCallResponse(
+ Collection extends SvamlCommand> commands) {
+ return new IncomingCallResponseCreator(Commands.append(null, commands));
+ }
+
+ /**
+ * Build the response to a call.incoming event
+ *
+ * @param sequence Lambda adding the commands to run
+ * @return The creator of the response
+ * @see #incomingCallResponse(SvamlCommand...)
+ * @since 2.4
+ */
+ public static IncomingCallResponseCreator incomingCallResponse(
+ Consumer sequence) {
+ return new IncomingCallResponseCreator(Commands.append(null, sequence));
+ }
+
+ /**
+ * Build the response to any other event: only the commands are taken into account
+ *
+ * The commands are not checked for gotoMenu: the event may have been triggered
+ * within a menu, where it is valid.
+ *
+ * @param commands Commands to run
+ * @return The response
+ * @since 2.4
+ */
+ public static VoiceSinchEventResponse response(SvamlCommand... commands) {
+ return response(Commands.append(null, commands));
+ }
+
+ /**
+ * Build the response to any other event: only the commands are taken into account
+ *
+ *
The commands are not checked for gotoMenu: the event may have been triggered
+ * within a menu, where it is valid.
+ *
+ * @param commands Commands to run
+ * @return The response
+ * @since 2.4
+ */
+ public static VoiceSinchEventResponse response(Collection extends SvamlCommand> commands) {
+ List list = Commands.append(null, commands);
+ return VoiceSinchEventResponse.builder().setCommands(list).build();
+ }
+
+ /**
+ * Build the response to any other event: only the commands are taken into account
+ *
+ * The commands are not checked for gotoMenu: the event may have been triggered
+ * within a menu, where it is valid.
+ *
+ * @param sequence Lambda adding the commands to run
+ * @return The response
+ * @since 2.4
+ */
+ public static VoiceSinchEventResponse response(Consumer sequence) {
+ return response(Commands.append(null, sequence));
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/StaticCallBehaviorCreator.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/StaticCallBehaviorCreator.java
new file mode 100644
index 000000000..62386b72d
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/StaticCallBehaviorCreator.java
@@ -0,0 +1,101 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.sinch.sdk.domains.voice.models.v2.services.StaticCallBehavior;
+import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEvents;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Fluent creator of a STATIC service call behavior
+ *
+ * Returned by {@link CallBehaviorHelper#staticBehavior(SvamlCommand...)}.
+ *
+ *
Calling {@link #onHangup} more than once appends the commands.
+ *
+ * @since 2.4
+ */
+public final class StaticCallBehaviorCreator {
+
+ private final List commands;
+ private String name;
+ private List onHangup;
+
+ StaticCallBehaviorCreator(List commands) {
+ this.commands = commands;
+ }
+
+ /**
+ * Set the name of the call, sent as callName
+ *
+ * @param name Name of the call, 1 to 32 characters without spaces
+ * @return This creator
+ * @since 2.4
+ */
+ public StaticCallBehaviorCreator name(String name) {
+ this.name = Objects.requireNonNull(name, "name must not be null");
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public StaticCallBehaviorCreator onHangup(SvamlCommand... commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param commands Commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public StaticCallBehaviorCreator onHangup(Collection extends SvamlCommand> commands) {
+ onHangup = Commands.append(onHangup, commands);
+ return this;
+ }
+
+ /**
+ * Add commands to run when the call is hung up
+ *
+ * @param sequence Lambda adding the commands to append
+ * @return This creator
+ * @since 2.4
+ */
+ public StaticCallBehaviorCreator onHangup(Consumer sequence) {
+ onHangup = Commands.append(onHangup, sequence);
+ return this;
+ }
+
+ /**
+ * Build the call behavior
+ *
+ * @return The STATIC call behavior
+ * @throws IllegalStateException if a gotoMenu command is outside a menu, or if a
+ * menu has a start menu or a gotoMenu target that is not one of its items
+ * @since 2.4
+ */
+ public StaticCallBehavior build() {
+ MenuCreator.checkGotoTargets(commands, Collections.emptySet());
+ MenuCreator.checkGotoTargets(onHangup, Collections.emptySet());
+ SvamlInput.Builder svaml = SvamlInput.builder().setCommands(Commands.copy(commands));
+ if (null != name) {
+ svaml.setCallName(name);
+ }
+ if (null != onHangup) {
+ svaml.setEvents(
+ IncomingCallResponseEvents.builder().setOnHangup(Commands.copy(onHangup)).build());
+ }
+ return StaticCallBehavior.builder().setStatic(svaml.build()).build();
+ }
+}
diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/package-info.java
new file mode 100644
index 000000000..058a2c323
--- /dev/null
+++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/helpers/package-info.java
@@ -0,0 +1,18 @@
+/**
+ * Helpers to build Voice V2 payloads
+ *
+ *
+ * - Call destinations and origins: {@link Destination}
+ *
- SVAML commands: {@link Calls}, {@link Amd}, {@link Messages}, {@link Recording}
+ *
- Custom events and menus: {@link CustomEvents} and {@link Menu}
+ *
- Command sequences: {@link CommandsSequenceCreator}
+ *
- Service call behaviors: {@link CallBehaviorHelper}
+ *
- Sinch Event responses: {@link SinchEventResponses}
+ *
+ *
+ * The limits of the API (lengths, ranges, number of items) are documented on the methods but not
+ * checked by the helpers: the API checks them.
+ *
+ * @since 2.4
+ */
+package com.sinch.sdk.domains.voice.models.v2.helpers;
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/AmdTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/AmdTest.java
new file mode 100644
index 000000000..6337b4059
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/AmdTest.java
@@ -0,0 +1,42 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.util.Collections;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class AmdTest extends HelpersBaseTest {
+
+ @Test
+ void activateWithoutHandlers() throws JsonProcessingException, JSONException {
+ assertJson(Amd.activate().build(), "{\"command\":\"amd\"}");
+ }
+
+ @Test
+ void activateAllHandlers() throws JsonProcessingException, JSONException {
+ assertJson(
+ Amd.activate()
+ .onHuman(Calls.bridgeCall("support"))
+ .onMachine(Calls.hangup())
+ .onBeep(Calls.pause(1500))
+ .onUnknown(Collections.singletonList(Calls.hangup("origin")))
+ .build(),
+ "{\"command\":\"amd\",\"events\":{"
+ + "\"onHuman\":[{\"command\":\"bridgeCall\",\"bridgeName\":\"support\"}],"
+ + "\"onMachine\":["
+ + HANGUP_JSON
+ + "],"
+ + "\"onBeep\":[{\"command\":\"pause\",\"durationMilliseconds\":1500}],"
+ + "\"onUnknown\":[{\"command\":\"hangup\",\"callName\":\"origin\"}]}}");
+ }
+
+ @Test
+ void handlersAppend() throws JsonProcessingException, JSONException {
+ assertJson(
+ Amd.activate().onMachine(Calls.pause(1200)).onMachine(Calls.hangup()).build(),
+ "{\"command\":\"amd\",\"events\":{\"onMachine\":["
+ + "{\"command\":\"pause\",\"durationMilliseconds\":1200},"
+ + HANGUP_JSON
+ + "]}}");
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelperTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelperTest.java
new file mode 100644
index 000000000..57e3a2b5b
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallBehaviorHelperTest.java
@@ -0,0 +1,132 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.services.StaticCallBehavior;
+import java.util.Collections;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class CallBehaviorHelperTest extends HelpersBaseTest {
+
+ @Test
+ void none() throws JsonProcessingException, JSONException {
+ assertJson(CallBehaviorHelper.none(), "{\"type\":\"NONE\"}");
+ }
+
+ @Test
+ void eventDestination() throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.eventDestination("https://example.com/events"),
+ "{\"type\":\"WEBHOOK\",\"webhook\":{\"url\":\"https://example.com/events\"}}");
+ }
+
+ @Test
+ void eventDestinationWithFallbackUrl() throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.eventDestination(
+ "https://example.com/events", "https://fallback.example.com/events"),
+ "{\"type\":\"WEBHOOK\",\"webhook\":{\"url\":\"https://example.com/events\","
+ + "\"fallbackUrl\":\"https://fallback.example.com/events\"}}");
+ }
+
+ @Test
+ void staticBehaviorOnlySetsCommands() throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.staticBehavior(Calls.answer(), Calls.hangup()).build(),
+ "{\"type\":\"STATIC\",\"static\":{\"commands\":[{\"command\":\"answer\"},"
+ + HANGUP_JSON
+ + "]}}");
+ }
+
+ @Test
+ void staticBehaviorAllFields() throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.staticBehavior(seq -> seq.answer().text("Hello", "Emma"))
+ .name("welcome")
+ .onHangup(Calls.hangup("origin"))
+ .onHangup(Collections.singletonList(Calls.pause(10)))
+ .build(),
+ "{\"type\":\"STATIC\",\"static\":{\"commands\":[{\"command\":\"answer\"},"
+ + "{\"command\":\"messages\",\"messages\":[{\"type\":\"SAY\",\"say\":"
+ + "{\"text\":\"Hello\",\"voiceName\":\"Emma\"}}]}],"
+ + "\"callName\":\"welcome\",\"events\":{\"onHangup\":["
+ + "{\"command\":\"hangup\",\"callName\":\"origin\"},"
+ + "{\"command\":\"pause\",\"durationMilliseconds\":10}]}}}");
+ }
+
+ @Test
+ void staticBehaviorFromCollectionAndOnHangupLambda()
+ throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.staticBehavior(Collections.singletonList(Calls.answer()))
+ .onHangup(seq -> seq.hangup())
+ .build(),
+ "{\"type\":\"STATIC\",\"static\":{\"commands\":[{\"command\":\"answer\"}],"
+ + "\"events\":{\"onHangup\":["
+ + HANGUP_JSON
+ + "]}}}");
+ }
+
+ @Test
+ void staticBehaviorRejectsGotoMenuOutsideAMenu() {
+ Assertions.assertThatThrownBy(
+ () -> CallBehaviorHelper.staticBehavior(seq -> seq.gotoMenu("main")).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("only valid within a menu");
+ Assertions.assertThatThrownBy(
+ () ->
+ CallBehaviorHelper.staticBehavior(Calls.answer())
+ .onHangup(Menu.gotoMenu("main"))
+ .build())
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void staticBehaviorAcceptsValidMenu() {
+ Assertions.assertThatCode(
+ () ->
+ CallBehaviorHelper.staticBehavior(
+ seq ->
+ seq.menu(
+ "main",
+ menu ->
+ menu.item(
+ "main", item -> item.onFailure(Menu.gotoMenu("main")))))
+ .build())
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void staticBehaviorKeepsExplicitlyEmptyOnHangup() throws JsonProcessingException, JSONException {
+ assertJson(
+ CallBehaviorHelper.staticBehavior(Calls.answer()).onHangup().build(),
+ "{\"type\":\"STATIC\",\"static\":{\"commands\":[{\"command\":\"answer\"}],"
+ + "\"events\":{\"onHangup\":[]}}}");
+ }
+
+ @Test
+ void staticBehaviorBuildIsNotAffectedByLaterChanges() {
+ StaticCallBehaviorCreator creator =
+ CallBehaviorHelper.staticBehavior(Calls.answer()).onHangup(Calls.hangup());
+ StaticCallBehavior first = creator.build();
+
+ creator.onHangup(Calls.pause(10)).name("later");
+
+ Assertions.assertThat(first.getStatic().getEvents().getOnHangup()).hasSize(1);
+ Assertions.assertThat(first.getStatic().getCallName()).isNull();
+ Assertions.assertThat(creator.build().getStatic().getEvents().getOnHangup()).hasSize(2);
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> CallBehaviorHelper.eventDestination(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> CallBehaviorHelper.eventDestination("https://example.com", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> CallBehaviorHelper.staticBehavior(Calls.answer()).name(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallsTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallsTest.java
new file mode 100644
index 000000000..48c92e94b
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CallsTest.java
@@ -0,0 +1,148 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
+import java.util.Arrays;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class CallsTest extends HelpersBaseTest {
+
+ @Test
+ void answer() throws JsonProcessingException, JSONException {
+ assertJson(Calls.answer(), "{\"command\":\"answer\"}");
+ }
+
+ @Test
+ void hangup() throws JsonProcessingException, JSONException {
+ assertJson(Calls.hangup(), HANGUP_JSON);
+ }
+
+ @Test
+ void hangupNamedLeg() throws JsonProcessingException, JSONException {
+ assertJson(Calls.hangup("customer"), "{\"command\":\"hangup\",\"callName\":\"customer\"}");
+ }
+
+ @Test
+ void pause() throws JsonProcessingException, JSONException {
+ assertJson(Calls.pause(1200), "{\"command\":\"pause\",\"durationMilliseconds\":1200}");
+ }
+
+ @Test
+ void bridgeCall() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.bridgeCall("support-queue"),
+ "{\"command\":\"bridgeCall\",\"bridgeName\":\"support-queue\"}");
+ }
+
+ @Test
+ void dialOnlySetsTo() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.dial(Destination.phone("+15551234567")).build(),
+ "{\"command\":\"dial\",\"to\":" + PHONE_JSON + "}");
+ }
+
+ @Test
+ void dialParsesStringDestination() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.dial("phone:+15551234567").build(),
+ "{\"command\":\"dial\",\"to\":" + PHONE_JSON + "}");
+ }
+
+ @Test
+ void dialAllFields() throws JsonProcessingException, JSONException {
+ DialCommand dial =
+ Calls.dial("sip:alice@example.com")
+ .from("+15551234567")
+ .name("customer")
+ .timeoutDurationSeconds(25)
+ .maxDurationSeconds(3600)
+ .onAnswer(Calls.answer())
+ .onBusy(Calls.pause(100))
+ .onReject(Calls.bridgeCall("queue"))
+ .onTimeout(Calls.hangup("other"))
+ .onHangup(Messages.stop("greeting"))
+ .onFailure(Calls.hangup())
+ .build();
+
+ assertJson(
+ dial,
+ "{\"command\":\"dial\","
+ + "\"to\":{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:alice@example.com\"}},"
+ + "\"from\":"
+ + PHONE_JSON
+ + ",\"callName\":\"customer\",\"dialTimeoutDurationSeconds\":25,"
+ + "\"maxCallDurationSeconds\":3600,"
+ + "\"events\":{"
+ + "\"onAnswer\":[{\"command\":\"answer\"}],"
+ + "\"onBusy\":[{\"command\":\"pause\",\"durationMilliseconds\":100}],"
+ + "\"onReject\":[{\"command\":\"bridgeCall\",\"bridgeName\":\"queue\"}],"
+ + "\"onTimeout\":[{\"command\":\"hangup\",\"callName\":\"other\"}],"
+ + "\"onHangup\":[{\"command\":\"stopMessages\",\"messagesName\":\"greeting\"}],"
+ + "\"onFailure\":[{\"command\":\"hangup\"}]}}");
+ }
+
+ @Test
+ void dialFromCallOrigin() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.dial("+15550000000").from(Destination.sipFrom("sip:bob@example.com")).build(),
+ "{\"command\":\"dial\","
+ + "\"to\":{\"type\":\"PHONE\",\"phone\":{\"number\":\"+15550000000\"}},"
+ + "\"from\":{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:bob@example.com\"}}}");
+ }
+
+ @Test
+ void dialHandlersAppend() throws JsonProcessingException, JSONException {
+ DialCommand dial =
+ Calls.dial("+15551234567")
+ .onAnswer(Calls.answer())
+ .onAnswer(Arrays.asList(Calls.pause(500), Calls.hangup()))
+ .build();
+
+ assertJson(
+ dial,
+ "{\"command\":\"dial\",\"to\":"
+ + PHONE_JSON
+ + ",\"events\":{\"onAnswer\":[{\"command\":\"answer\"},"
+ + "{\"command\":\"pause\",\"durationMilliseconds\":500},"
+ + HANGUP_JSON
+ + "]}}");
+ }
+
+ @Test
+ void dialKeepsExplicitlyEmptyHandler() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.dial("+15551234567").onBusy().build(),
+ "{\"command\":\"dial\",\"to\":" + PHONE_JSON + ",\"events\":{\"onBusy\":[]}}");
+ }
+
+ @Test
+ void dialBuildIsNotAffectedByLaterChanges() {
+ DialCreator creator = Calls.dial("+15551234567").onAnswer(Calls.answer());
+ DialCommand first = creator.build();
+
+ creator.onAnswer(Calls.hangup());
+
+ Assertions.assertThat(first.getEvents().getOnAnswer()).hasSize(1);
+ Assertions.assertThat(creator.build().getEvents().getOnAnswer()).hasSize(2);
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> Calls.dial((CallDestination) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Calls.dial((String) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Calls.dial("+15551234567").name(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Calls.dial("+15551234567").onAnswer((SvamlCommand) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Calls.bridgeCall(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Calls.hangup(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreatorTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreatorTest.java
new file mode 100644
index 000000000..b003d7630
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CommandsSequenceCreatorTest.java
@@ -0,0 +1,379 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingFormatType;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class CommandsSequenceCreatorTest extends HelpersBaseTest {
+
+ void assertSameJson(Object actual, Object expected)
+ throws JsonProcessingException, JSONException {
+ assertJson(actual, objectMapper.writeValueAsString(expected));
+ }
+
+ @Test
+ void simpleMethodsAddTheHelperCommand() throws JsonProcessingException, JSONException {
+ List sequence =
+ CommandsSequenceCreator.create()
+ .answer()
+ .bridgeCall("queue")
+ .dial("+15551234567")
+ .dial(Destination.phone("+15550000000"))
+ .hangup()
+ .hangup("leg")
+ .pause(100)
+ .text("Hello", "Emma")
+ .ssml("Hello", "Emma")
+ .play("https://example.com/a.mp3")
+ .stopMessages("greeting")
+ .recording(RecordingDestinationType.AWS, "s3://b", "c")
+ .recording(Recording.options(RecordingDestinationType.GCP, "gs://b", "c"))
+ .stopRecording("rec")
+ .customEvents("event", "https://example.com")
+ .customEvents("event", "https://example.com", "https://fallback.example.com")
+ .command(Calls.pause(200), Calls.answer())
+ .build();
+
+ assertSameJson(
+ sequence,
+ Arrays.asList(
+ Calls.answer(),
+ Calls.bridgeCall("queue"),
+ Calls.dial("+15551234567").build(),
+ Calls.dial(Destination.phone("+15550000000")).build(),
+ Calls.hangup(),
+ Calls.hangup("leg"),
+ Calls.pause(100),
+ Messages.text("Hello", "Emma"),
+ Messages.ssml("Hello", "Emma"),
+ Messages.play("https://example.com/a.mp3"),
+ Messages.stop("greeting"),
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c").build(),
+ Recording.start(Recording.options(RecordingDestinationType.GCP, "gs://b", "c")).build(),
+ Recording.stop("rec"),
+ CustomEvents.trigger("event", "https://example.com"),
+ CustomEvents.trigger("event", "https://example.com", "https://fallback.example.com"),
+ Calls.pause(200),
+ Calls.answer()));
+ }
+
+ @Test
+ void lambdaMethodsAddTheCreatorCommand() throws JsonProcessingException, JSONException {
+ List sequence =
+ CommandsSequenceCreator.create()
+ .dial("+15551234567", dial -> dial.name("customer").onBusy(Calls.hangup()))
+ .amd(amd -> amd.onMachine(Calls.hangup()))
+ .messages(messages -> messages.text("Hi", "Emma").name("greeting"))
+ .recording(
+ RecordingDestinationType.AWS,
+ "s3://b",
+ "c",
+ recording -> recording.format(RecordingFormatType.WAV).name("rec"))
+ .recording(
+ Recording.options(RecordingDestinationType.GCP, "gs://b", "c"),
+ recording -> recording.name("other"))
+ .menu("main", menu -> menu.item("main", item -> item.match("1", Calls.hangup())))
+ .build();
+
+ assertSameJson(
+ sequence,
+ Arrays.asList(
+ Calls.dial("+15551234567").name("customer").onBusy(Calls.hangup()).build(),
+ Amd.activate().onMachine(Calls.hangup()).build(),
+ Messages.start().text("Hi", "Emma").name("greeting").build(),
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .format(RecordingFormatType.WAV)
+ .name("rec")
+ .build(),
+ Recording.start(Recording.options(RecordingDestinationType.GCP, "gs://b", "c"))
+ .name("other")
+ .build(),
+ Menu.create("main").item("main", item -> item.match("1", Calls.hangup())).build()));
+ }
+
+ @Test
+ void handlersAcceptASequenceLambda() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ Calls.dial("+15551234567")
+ .onAnswer(answered -> answered.answer().text("Hi", "Emma"))
+ .onAnswer(more -> more.hangup())
+ .build(),
+ Calls.dial("+15551234567")
+ .onAnswer(Calls.answer(), Messages.text("Hi", "Emma"), Calls.hangup())
+ .build());
+ assertSameJson(
+ Amd.activate().onHuman(human -> human.bridgeCall("queue")).build(),
+ Amd.activate().onHuman(Calls.bridgeCall("queue")).build());
+ assertSameJson(
+ Messages.start().play("https://example.com/a.mp3").onFinish(done -> done.hangup()).build(),
+ Messages.start().play("https://example.com/a.mp3").onFinish(Calls.hangup()).build());
+ assertSameJson(
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .onFinish(done -> done.hangup())
+ .onFailure(failed -> failed.pause(10))
+ .build(),
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .onFinish(Calls.hangup())
+ .onFailure(Calls.pause(10))
+ .build());
+ }
+
+ @Test
+ void menuMatchAndOnFailureAcceptASequenceLambda() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.match("1", one -> one.text("Bye", "Emma").hangup())
+ .match("2", two -> two.gotoMenu("support"))
+ .onFailure(failed -> failed.hangup()))
+ .item("support", item -> item.onFailure(failed -> failed.gotoMenu("main")))
+ .build(),
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.match("1", Messages.text("Bye", "Emma"), Calls.hangup())
+ .match("2", Menu.gotoMenu("support"))
+ .onFailure(Calls.hangup()))
+ .item("support", item -> item.onFailure(Menu.gotoMenu("main")))
+ .build());
+ }
+
+ @Test
+ void supportCallFlow() throws JsonProcessingException, JSONException {
+ StartCallRequest request =
+ StartCallRequest.builder()
+ .setCommands(
+ CommandsSequenceCreator.create()
+ .dial(
+ "+34600111222",
+ dial ->
+ dial.name("customer")
+ .from("+34910000000")
+ .timeoutDurationSeconds(25)
+ .onAnswer(
+ answered ->
+ answered.amd(
+ amd ->
+ amd.onHuman(
+ human ->
+ human
+ .text(
+ "Connecting you to support,"
+ + " one moment.",
+ "Emma")
+ .recording(
+ RecordingDestinationType.AWS,
+ "s3://voice-recordings/support",
+ "key:secret:eu-west-1",
+ recording ->
+ recording.name(
+ "support-call"))
+ .bridgeCall("support-queue"))
+ .onMachine(
+ machine ->
+ machine
+ .pause(1200)
+ .text(
+ "This is a call from support,"
+ + " please call us back.",
+ "Emma")
+ .hangup())))
+ .onHangup(hungUp -> hungUp.stopRecording("support-call")))
+ .build())
+ .build();
+
+ assertJson(request, ComposedFlowTest.EXPECTED);
+ }
+
+ @Test
+ void gotoMenuWithinAMenuOfTheSequenceIsValid() {
+ Assertions.assertThatCode(
+ () ->
+ CommandsSequenceCreator.create()
+ .menu(
+ "main",
+ menu ->
+ menu.item(
+ "main",
+ item ->
+ item.onFailure(
+ failed ->
+ failed.dial(
+ "+15551234567",
+ dial ->
+ dial.onBusy(busy -> busy.gotoMenu("main"))))))
+ .build())
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void rejectsGotoMenuOutsideAMenu() {
+ Assertions.assertThatThrownBy(() -> CommandsSequenceCreator.create().gotoMenu("main").build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("only valid within a menu");
+ }
+
+ @Test
+ void rejectsGotoMenuOutsideAMenuInNestedHandler() {
+ Assertions.assertThatThrownBy(
+ () ->
+ CommandsSequenceCreator.create()
+ .dial("+15551234567", dial -> dial.onAnswer(answered -> answered.gotoMenu("x")))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("only valid within a menu");
+ }
+
+ @Test
+ void rejectsInvalidMenuAddedAsCommand() {
+ Assertions.assertThatThrownBy(
+ () ->
+ CommandsSequenceCreator.create()
+ .command(MenuCommand.builder().setStartMenu("missing").build())
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void buildReturnsANewList() {
+ CommandsSequenceCreator creator = CommandsSequenceCreator.create().answer();
+ List first = creator.build();
+
+ first.add(Calls.hangup());
+ creator.hangup("leg");
+
+ Assertions.assertThat(first).hasSize(2);
+ Assertions.assertThat(creator.build()).hasSize(2);
+ }
+
+ @Test
+ void everyDialHandlerAcceptsASequenceLambda() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ Calls.dial("+15551234567")
+ .onAnswer(s -> s.answer())
+ .onBusy(s -> s.pause(1))
+ .onReject(s -> s.pause(2))
+ .onTimeout(s -> s.pause(3))
+ .onHangup(s -> s.pause(4))
+ .onFailure(s -> s.hangup())
+ .build(),
+ Calls.dial("+15551234567")
+ .onAnswer(Calls.answer())
+ .onBusy(Calls.pause(1))
+ .onReject(Calls.pause(2))
+ .onTimeout(Calls.pause(3))
+ .onHangup(Calls.pause(4))
+ .onFailure(Calls.hangup())
+ .build());
+ }
+
+ @Test
+ void everyAmdHandlerAcceptsASequenceLambda() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ Amd.activate()
+ .onHuman(s -> s.answer())
+ .onMachine(s -> s.hangup())
+ .onBeep(s -> s.pause(1))
+ .onUnknown(s -> s.pause(2))
+ .build(),
+ Amd.activate()
+ .onHuman(Calls.answer())
+ .onMachine(Calls.hangup())
+ .onBeep(Calls.pause(1))
+ .onUnknown(Calls.pause(2))
+ .build());
+ }
+
+ @Test
+ void emptySequenceLambdaIsSentAsEmptyList() throws JsonProcessingException, JSONException {
+ assertJson(
+ Calls.dial("+15551234567").onBusy(s -> {}).build(),
+ "{\"command\":\"dial\",\"to\":" + PHONE_JSON + ",\"events\":{\"onBusy\":[]}}");
+ }
+
+ @Test
+ void rejectsNullSequenceLambda() {
+ Assertions.assertThatThrownBy(
+ () -> Calls.dial("+15551234567").onAnswer((Consumer) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> Amd.activate().onHuman((Consumer) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> Messages.start().onFinish((Consumer) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () ->
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .onFailure((Consumer) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> Menu.item(item -> item.match("1", (Consumer) null)))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void messageShortcutsAcceptNameAndOnFinish() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ CommandsSequenceCreator.create()
+ .text("Hello", "Emma", m -> m.name("greeting").onFinish(Calls.hangup()))
+ .ssml("Hi", "Emma", m -> m.name("ssml"))
+ .play("https://example.com/a.mp3", m -> m.onFinish(s -> s.hangup()))
+ .build(),
+ Arrays.asList(
+ Messages.text("Hello", "Emma", m -> m.name("greeting").onFinish(Calls.hangup())),
+ Messages.ssml("Hi", "Emma", m -> m.name("ssml")),
+ Messages.play("https://example.com/a.mp3", m -> m.onFinish(Calls.hangup()))));
+ }
+
+ @Test
+ void commandAcceptsACollection() throws JsonProcessingException, JSONException {
+ assertSameJson(
+ CommandsSequenceCreator.create()
+ .command(Arrays.asList(Calls.answer(), Calls.hangup()))
+ .build(),
+ Arrays.asList(Calls.answer(), Calls.hangup()));
+ }
+
+ @Test
+ void messageShortcutsAcceptAVoiceNameFromTheCatalog()
+ throws JsonProcessingException, JSONException {
+ assertSameJson(
+ CommandsSequenceCreator.create()
+ .text("Hello", VoiceName.EMMA)
+ .ssml("Hi", VoiceName.EMMA)
+ .text("Bye", VoiceName.EMMA, m -> m.onFinish(Calls.hangup()))
+ .ssml("Bye", VoiceName.EMMA, m -> m.name("bye"))
+ .build(),
+ Arrays.asList(
+ Messages.text("Hello", "Emma"),
+ Messages.ssml("Hi", "Emma"),
+ Messages.text("Bye", "Emma", m -> m.onFinish(Calls.hangup())),
+ Messages.ssml("Bye", "Emma", m -> m.name("bye"))));
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> CommandsSequenceCreator.create().amd(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> CommandsSequenceCreator.create().dial("+15551234567", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> CommandsSequenceCreator.create().command((SvamlCommand) null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/ComposedFlowTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/ComposedFlowTest.java
new file mode 100644
index 000000000..d2dd93c91
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/ComposedFlowTest.java
@@ -0,0 +1,71 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import java.util.Collections;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+/** The "Voice V2 Helpers meeting discussion" Confluence example, built with the helpers */
+class ComposedFlowTest extends HelpersBaseTest {
+
+ static final String EXPECTED =
+ "{\"commands\":[{\"command\":\"dial\",\"callName\":\"customer\","
+ + "\"from\":{\"type\":\"PHONE\",\"phone\":{\"number\":\"+34910000000\"}},"
+ + "\"to\":{\"type\":\"PHONE\",\"phone\":{\"number\":\"+34600111222\"}},"
+ + "\"dialTimeoutDurationSeconds\":25,"
+ + "\"events\":{"
+ + "\"onAnswer\":[{\"command\":\"amd\",\"events\":{"
+ + "\"onHuman\":["
+ + "{\"command\":\"messages\",\"messages\":[{\"type\":\"SAY\",\"say\":"
+ + "{\"text\":\"Connecting you to support, one moment.\",\"voiceName\":\"Emma\"}}]},"
+ + "{\"command\":\"startRecording\",\"recordingName\":\"support-call\","
+ + "\"recordingOptions\":{\"destination\":\"AWS\","
+ + "\"destinationUrl\":\"s3://voice-recordings/support\","
+ + "\"credentials\":\"key:secret:eu-west-1\"}},"
+ + "{\"command\":\"bridgeCall\",\"bridgeName\":\"support-queue\"}],"
+ + "\"onMachine\":["
+ + "{\"command\":\"pause\",\"durationMilliseconds\":1200},"
+ + "{\"command\":\"messages\",\"messages\":[{\"type\":\"SAY\",\"say\":"
+ + "{\"text\":\"This is a call from support, please call us back.\","
+ + "\"voiceName\":\"Emma\"}}]},"
+ + "{\"command\":\"hangup\"}]}}],"
+ + "\"onHangup\":[{\"command\":\"stopRecording\",\"recordingName\":\"support-call\"}]"
+ + "}}]}";
+
+ @Test
+ void supportCallFlow() throws JsonProcessingException, JSONException {
+ StartCallRequest request =
+ StartCallRequest.builder()
+ .setCommands(
+ Collections.singletonList(
+ Calls.dial("+34600111222")
+ .name("customer")
+ .from("+34910000000")
+ .timeoutDurationSeconds(25)
+ .onAnswer(
+ Amd.activate()
+ .onHuman(
+ Messages.text("Connecting you to support, one moment.", "Emma"),
+ Recording.start(
+ RecordingDestinationType.AWS,
+ "s3://voice-recordings/support",
+ "key:secret:eu-west-1")
+ .name("support-call")
+ .build(),
+ Calls.bridgeCall("support-queue"))
+ .onMachine(
+ Calls.pause(1200),
+ Messages.text(
+ "This is a call from support, please call us back.",
+ "Emma"),
+ Calls.hangup())
+ .build())
+ .onHangup(Recording.stop("support-call"))
+ .build()))
+ .build();
+
+ assertJson(request, EXPECTED);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEventsTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEventsTest.java
new file mode 100644
index 000000000..efeb7ff63
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/CustomEventsTest.java
@@ -0,0 +1,37 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class CustomEventsTest extends HelpersBaseTest {
+
+ @Test
+ void trigger() throws JsonProcessingException, JSONException {
+ assertJson(
+ CustomEvents.trigger("my.custom.event", "https://example.com/events"),
+ "{\"command\":\"webhook\",\"webhookName\":\"my.custom.event\","
+ + "\"url\":\"https://example.com/events\"}");
+ }
+
+ @Test
+ void triggerWithFallbackUrl() throws JsonProcessingException, JSONException {
+ assertJson(
+ CustomEvents.trigger(
+ "my.custom.event", "https://example.com/events", "https://fallback.example.com/events"),
+ "{\"command\":\"webhook\",\"webhookName\":\"my.custom.event\","
+ + "\"url\":\"https://example.com/events\","
+ + "\"fallbackUrl\":\"https://fallback.example.com/events\"}");
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> CustomEvents.trigger(null, "https://example.com"))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> CustomEvents.trigger("name", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> CustomEvents.trigger("name", "https://example.com", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/DestinationTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/DestinationTest.java
new file mode 100644
index 000000000..9bb8cd294
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/DestinationTest.java
@@ -0,0 +1,305 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.BaseTest;
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallHeader;
+import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin;
+import com.sinch.sdk.domains.voice.models.v2.destination.Phone;
+import com.sinch.sdk.domains.voice.models.v2.destination.Sip;
+import com.sinch.sdk.domains.voice.models.v2.destination.SipCallHeader;
+import com.sinch.sdk.domains.voice.models.v2.destination.SipFrom;
+import com.sinch.sdk.domains.voice.models.v2.destination.Stream;
+import com.sinch.sdk.domains.voice.models.v2.destination.StreamOptions;
+import com.sinch.sdk.domains.voice.models.v2.destination.VoiceRelay;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
+import java.util.Collections;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+class DestinationTest extends BaseTest {
+
+ static final String PHONE_JSON = "{\"type\":\"PHONE\",\"phone\":{\"number\":\"+15551234567\"}}";
+ static final String SIP_JSON =
+ "{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:user@example.com\"}}";
+ static final String SIPS_JSON =
+ "{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sips:user@example.com\"}}";
+ static final String STREAM_JSON =
+ "{\"type\":\"STREAM\",\"stream\":{\"endpoint\":\"wss://example.com/audio\"}}";
+
+ void assertJson(Object value, String expected) throws JsonProcessingException, JSONException {
+ JSONAssert.assertEquals(expected, objectMapper.writeValueAsString(value), true);
+ }
+
+ @Test
+ void phone() throws JsonProcessingException, JSONException {
+ Phone destination = Destination.phone("+15551234567");
+
+ assertJson(destination, PHONE_JSON);
+ }
+
+ @Test
+ void sip() throws JsonProcessingException, JSONException {
+ Sip destination = Destination.sip("sip:user@example.com");
+
+ assertJson(destination, SIP_JSON);
+ }
+
+ @Test
+ void sipKeepsEndpointWithoutScheme() throws JsonProcessingException, JSONException {
+ Sip destination = Destination.sip("user@example.com");
+
+ assertJson(destination, "{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"user@example.com\"}}");
+ }
+
+ @Test
+ void sipFrom() throws JsonProcessingException, JSONException {
+ SipFrom origin = Destination.sipFrom("sip:user@example.com");
+
+ assertJson(origin, SIP_JSON);
+ }
+
+ @Test
+ void stream() throws JsonProcessingException, JSONException {
+ Stream destination = Destination.stream("wss://example.com/audio");
+
+ assertJson(destination, STREAM_JSON);
+ }
+
+ @Test
+ void voiceRelay() throws JsonProcessingException, JSONException {
+ VoiceRelay destination = Destination.voiceRelay("wss://example.com/agent", "Emma", "en-US");
+
+ assertJson(
+ destination,
+ "{\"type\":\"VOICE_RELAY\",\"voiceRelay\":{\"endpoint\":\"wss://example.com/agent\","
+ + "\"ttsVoice\":\"Emma\",\"sttLanguage\":\"en-US\"}}");
+ }
+
+ @Test
+ void voiceRelayAcceptsVoiceOutsideCatalog() {
+ VoiceRelay destination =
+ Destination.voiceRelay("wss://example.com/agent", "FutureVoice", "en-US");
+
+ Assertions.assertThat(destination.getTtsVoice()).isEqualTo("FutureVoice");
+ }
+
+ @Test
+ void ofUntaggedIsPhone() throws JsonProcessingException, JSONException {
+ assertJson(Destination.of("+15551234567"), PHONE_JSON);
+ }
+
+ @Test
+ void ofStripsPhoneTag() throws JsonProcessingException, JSONException {
+ assertJson(Destination.of("phone:+15551234567"), PHONE_JSON);
+ }
+
+ @Test
+ void ofKeepsSipEndpoint() throws JsonProcessingException, JSONException {
+ assertJson(Destination.of("sip:user@example.com"), SIP_JSON);
+ }
+
+ @Test
+ void ofKeepsSipsEndpoint() throws JsonProcessingException, JSONException {
+ assertJson(Destination.of("sips:user@example.com"), SIPS_JSON);
+ }
+
+ @Test
+ void ofStripsStreamTag() throws JsonProcessingException, JSONException {
+ assertJson(Destination.of("stream:wss://example.com/audio"), STREAM_JSON);
+ }
+
+ @Test
+ void ofKeepsUnknownPrefixAsPhoneNumber() throws JsonProcessingException, JSONException {
+ assertJson(
+ Destination.of("tel:+15551234567"),
+ "{\"type\":\"PHONE\",\"phone\":{\"number\":\"tel:+15551234567\"}}");
+ }
+
+ @Test
+ void ofKeepsBatchPlaceholderAsPhoneNumber() throws JsonProcessingException, JSONException {
+ assertJson(
+ Destination.of("@toNumber"), "{\"type\":\"PHONE\",\"phone\":{\"number\":\"@toNumber\"}}");
+ }
+
+ @Test
+ void ofIsBothCallOriginAndCallDestination() {
+ BaseDestination endpoint = Destination.of("sip:user@example.com");
+
+ Assertions.assertThat(endpoint).isInstanceOf(CallOrigin.class);
+ Assertions.assertThat(endpoint).isInstanceOf(CallDestination.class);
+ }
+
+ @Test
+ void ofComparesByValue() {
+ Assertions.assertThat(Destination.of("phone:+15551234567"))
+ .isEqualTo(Destination.of("+15551234567"))
+ .hasSameHashCodeAs(Destination.of("+15551234567"))
+ .isNotEqualTo(Destination.of("sip:user@example.com"));
+ }
+
+ @Test
+ void ofToStringIsTheWrappedModel() {
+ Assertions.assertThat(Destination.of("+15551234567"))
+ .hasToString(Destination.phone("+15551234567").toString());
+ }
+
+ @Test
+ void ofValuesAreAcceptedAsDialFromAndTo() throws JsonProcessingException, JSONException {
+ DialCommand dial =
+ DialCommand.builder()
+ .setFrom(Destination.of("sip:15550001111@sip.sinch.com"))
+ .setTo(Destination.of("sip:15550002222@sip.sinch.com"))
+ .build();
+
+ assertJson(
+ dial,
+ "{\"command\":\"dial\","
+ + "\"from\":{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:15550001111@sip.sinch.com\"}},"
+ + "\"to\":{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:15550002222@sip.sinch.com\"}}}");
+ }
+
+ @Test
+ void ofValuesAreSerializedInStartCallRequest() throws JsonProcessingException, JSONException {
+ StartCallRequest request =
+ StartCallRequest.builder()
+ .setCommands(
+ Collections.singletonList(
+ DialCommand.builder()
+ .setFrom(Destination.of("+15550001111"))
+ .setTo(Destination.of("stream:wss://example.com/audio"))
+ .build()))
+ .build();
+
+ assertJson(
+ request,
+ "{\"commands\":[{\"command\":\"dial\","
+ + "\"from\":{\"type\":\"PHONE\",\"phone\":{\"number\":\"+15550001111\"}},"
+ + "\"to\":{\"type\":\"STREAM\",\"stream\":{\"endpoint\":\"wss://example.com/audio\"}}}]}");
+ }
+
+ @Test
+ void sipWithOptionalFields() throws JsonProcessingException, JSONException {
+ Sip destination =
+ Destination.sip(
+ "sip:user@example.com",
+ sip ->
+ sip.setTransport(Sip.TransportEnum.TCP)
+ .setCallHeaders(
+ Collections.singletonList(
+ SipCallHeader.builder().setKey("X-Id").setValue("1").build())));
+
+ assertJson(
+ destination,
+ "{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:user@example.com\",\"transport\":\"TCP\","
+ + "\"callHeaders\":[{\"key\":\"X-Id\",\"value\":\"1\"}]}}");
+ }
+
+ @Test
+ void sipFromWithOptionalFields() throws JsonProcessingException, JSONException {
+ SipFrom origin =
+ Destination.sipFrom("sip:user@example.com", sip -> sip.setDisplayName("Alice"));
+
+ assertJson(
+ origin,
+ "{\"type\":\"SIP\",\"sip\":{\"endpoint\":\"sip:user@example.com\","
+ + "\"displayName\":\"Alice\"}}");
+ }
+
+ @Test
+ void streamWithOptionalFields() throws JsonProcessingException, JSONException {
+ Stream destination =
+ Destination.stream(
+ "wss://example.com/audio",
+ stream ->
+ stream
+ .setStreamOptions(
+ StreamOptions.builder()
+ .setSampleRate(StreamOptions.SampleRateEnum.HZ_16000)
+ .build())
+ .setCallHeaders(
+ Collections.singletonList(
+ CallHeader.builder().setKey("X-Id").setValue("1").build())));
+
+ assertJson(
+ destination,
+ "{\"type\":\"STREAM\",\"stream\":{\"endpoint\":\"wss://example.com/audio\","
+ + "\"streamOptions\":{\"sampleRate\":16000},"
+ + "\"callHeaders\":[{\"key\":\"X-Id\",\"value\":\"1\"}]}}");
+ }
+
+ @Test
+ void voiceRelayWithOptionalFields() throws JsonProcessingException, JSONException {
+ VoiceRelay destination =
+ Destination.voiceRelay(
+ "wss://example.com/agent",
+ "Emma",
+ "en-US",
+ relay -> relay.setEnableInterruptions(false));
+
+ assertJson(
+ destination,
+ "{\"type\":\"VOICE_RELAY\",\"voiceRelay\":{\"endpoint\":\"wss://example.com/agent\","
+ + "\"ttsVoice\":\"Emma\",\"sttLanguage\":\"en-US\",\"enableInterruptions\":false}}");
+ }
+
+ @Test
+ void emptyLambdaIsTheSameAsTheSimpleFactory() {
+ Assertions.assertThat(Destination.sip("sip:user@example.com", sip -> {}))
+ .isEqualTo(Destination.sip("sip:user@example.com"));
+ Assertions.assertThat(Destination.stream("wss://example.com/audio", stream -> {}))
+ .isEqualTo(Destination.stream("wss://example.com/audio"));
+ }
+
+ @Test
+ void rejectsNullLambda() {
+ Assertions.assertThatThrownBy(() -> Destination.sip("sip:user@example.com", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.sipFrom("sip:user@example.com", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.stream("wss://example.com/audio", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> Destination.voiceRelay("wss://example.com", "Emma", "en-US", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void voiceRelayAcceptsAVoiceNameFromTheCatalog() {
+ Assertions.assertThat(
+ Destination.voiceRelay("wss://example.com/agent", VoiceName.EMMA, "en-US"))
+ .isEqualTo(Destination.voiceRelay("wss://example.com/agent", "Emma", "en-US"));
+ Assertions.assertThat(
+ Destination.voiceRelay(
+ "wss://example.com/agent",
+ VoiceName.EMMA,
+ "en-US",
+ relay -> relay.setEnableInterruptions(false)))
+ .isEqualTo(
+ Destination.voiceRelay(
+ "wss://example.com/agent",
+ "Emma",
+ "en-US",
+ relay -> relay.setEnableInterruptions(false)));
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> Destination.of(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.phone(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.sip(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.sipFrom(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.stream(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Destination.voiceRelay("wss://example.com", "Emma", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/HelpersBaseTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/HelpersBaseTest.java
new file mode 100644
index 000000000..847ad73e7
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/HelpersBaseTest.java
@@ -0,0 +1,16 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.BaseTest;
+import org.json.JSONException;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+abstract class HelpersBaseTest extends BaseTest {
+
+ static final String PHONE_JSON = "{\"type\":\"PHONE\",\"phone\":{\"number\":\"+15551234567\"}}";
+ static final String HANGUP_JSON = "{\"command\":\"hangup\"}";
+
+ void assertJson(Object value, String expected) throws JsonProcessingException, JSONException {
+ JSONAssert.assertEquals(expected, objectMapper.writeValueAsString(value), true);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MenuTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MenuTest.java
new file mode 100644
index 000000000..b02485a0d
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MenuTest.java
@@ -0,0 +1,330 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuItem;
+import com.sinch.sdk.domains.voice.models.v2.svaml.menu.MenuPrompt;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class MenuTest extends HelpersBaseTest {
+
+ static final String PROMPT_JSON =
+ "{\"messages\":[{\"type\":\"SAY\",\"say\":{\"text\":\"Press 1\",\"voiceName\":\"Emma\"}}]}";
+
+ static final Consumer PRESS_1 = prompt -> prompt.text("Press 1", "Emma");
+
+ @Test
+ void gotoMenu() throws JsonProcessingException, JSONException {
+ assertJson(Menu.gotoMenu("billing"), "{\"command\":\"gotoMenu\",\"menuName\":\"billing\"}");
+ }
+
+ @Test
+ void createWithOneItem() throws JsonProcessingException, JSONException {
+ assertJson(
+ Menu.create("main").item("main", item -> item.prompt(PRESS_1)).build(),
+ "{\"command\":\"menu\",\"startMenu\":\"main\",\"menus\":{\"main\":{\"prompt\":"
+ + PROMPT_JSON
+ + "}}}");
+ }
+
+ @Test
+ void itemAllFields() throws JsonProcessingException, JSONException {
+ MenuCommand menu =
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.prompt(
+ prompt -> prompt.play("https://example.com/a.mp3").allowBargeIn(false))
+ .repeatPrompt(PRESS_1)
+ .inputTimeoutDurationSeconds(10)
+ .repeatCount(3)
+ .minimumInputLength(1)
+ .maximumInputLength(4)
+ .terminatingSequence("#")
+ .inputMethods(MenuItem.InputMethodsEnum.DTMF)
+ .match("1", Menu.gotoMenu("main"))
+ .onFailure(Calls.hangup("origin")))
+ .build();
+
+ assertJson(
+ menu,
+ "{\"command\":\"menu\",\"startMenu\":\"main\",\"menus\":{\"main\":{"
+ + "\"prompt\":{\"allowBargeIn\":false,\"messages\":[{\"type\":\"PLAY\","
+ + "\"play\":{\"url\":\"https://example.com/a.mp3\"}}]},"
+ + "\"repeatPrompt\":"
+ + PROMPT_JSON
+ + ",\"inputTimeoutDurationSeconds\":10,\"repeatCount\":3,"
+ + "\"minimumInputLength\":1,\"maximumInputLength\":4,\"terminatingSequence\":\"#\","
+ + "\"inputMethods\":[\"DTMF\"],"
+ + "\"matches\":{\"1\":[{\"command\":\"gotoMenu\",\"menuName\":\"main\"}]},"
+ + "\"onFailure\":[{\"command\":\"hangup\",\"callName\":\"origin\"}]}}}");
+ }
+
+ @Test
+ void matchesAppendPerInput() throws JsonProcessingException, JSONException {
+ MenuCommand menu =
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.match("1", Calls.pause(500))
+ .match("1", Arrays.asList(Calls.hangup()))
+ .match("2", Calls.answer()))
+ .build();
+
+ assertJson(
+ menu,
+ "{\"command\":\"menu\",\"startMenu\":\"main\",\"menus\":{\"main\":{\"matches\":{"
+ + "\"1\":[{\"command\":\"pause\",\"durationMilliseconds\":500},"
+ + HANGUP_JSON
+ + "],\"2\":[{\"command\":\"answer\"}]}}}}");
+ }
+
+ @Test
+ void sharedPromptAndPrebuiltItem() throws JsonProcessingException, JSONException {
+ MenuPrompt shared = Menu.prompt(PRESS_1);
+ MenuItem support = MenuItem.builder().setPrompt(shared).build();
+
+ assertJson(
+ Menu.create("main")
+ .item("main", item -> item.prompt(shared).repeatPrompt(shared))
+ .item("support", support)
+ .build(),
+ "{\"command\":\"menu\",\"startMenu\":\"main\",\"menus\":{"
+ + "\"main\":{\"prompt\":"
+ + PROMPT_JSON
+ + ",\"repeatPrompt\":"
+ + PROMPT_JSON
+ + "},\"support\":{\"prompt\":"
+ + PROMPT_JSON
+ + "}}}");
+ }
+
+ @Test
+ void gotoAcrossItemsIsValid() {
+ Assertions.assertThatCode(
+ () ->
+ Menu.create("main")
+ .item("main", item -> item.match("2", Menu.gotoMenu("support")))
+ .item("support", item -> item.onFailure(Menu.gotoMenu("main")))
+ .build())
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void promptRequiresAMessage() {
+ Assertions.assertThatThrownBy(() -> Menu.prompt(prompt -> prompt.allowBargeIn(true)))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void rejectsDuplicatedItem() {
+ Assertions.assertThatThrownBy(
+ () -> Menu.create("main").item("main", item -> {}).item("main", item -> {}))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("\"main\"");
+ }
+
+ @Test
+ void duplicatedItemLambdaIsNotRun() {
+ AtomicBoolean run = new AtomicBoolean();
+ MenuCreator menu = Menu.create("main").item("main", item -> {});
+
+ Assertions.assertThatThrownBy(() -> menu.item("main", item -> run.set(true)))
+ .isInstanceOf(IllegalArgumentException.class);
+ Assertions.assertThat(run).isFalse();
+ }
+
+ @Test
+ void promptWithSsml() throws JsonProcessingException, JSONException {
+ assertJson(
+ Menu.prompt(prompt -> prompt.ssml("Press 1", "Emma")),
+ "{\"messages\":[{\"type\":\"SAY\",\"say\":{\"text\":\"Press 1\","
+ + "\"voiceName\":\"Emma\",\"format\":\"SSML\"}}]}");
+ }
+
+ @Test
+ void reusableItem() throws JsonProcessingException, JSONException {
+ MenuItem shared = Menu.item(item -> item.prompt(PRESS_1).onFailure(Calls.hangup()));
+
+ assertJson(
+ Menu.create("main").item("main", shared).build(),
+ "{\"command\":\"menu\",\"startMenu\":\"main\",\"menus\":{\"main\":{\"prompt\":"
+ + PROMPT_JSON
+ + ",\"onFailure\":["
+ + HANGUP_JSON
+ + "]}}}");
+ }
+
+ @Test
+ void rejectsUndefinedGotoTargetInPrebuiltItem() {
+ MenuItem item =
+ MenuItem.builder()
+ .setMatches(
+ Collections.singletonMap("1", Collections.singletonList(Menu.gotoMenu("missing"))))
+ .build();
+
+ Assertions.assertThatThrownBy(() -> Menu.create("main").item("main", item).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void rejectsUndefinedGotoTargetInMessagesOnFinish() {
+ Assertions.assertThatThrownBy(
+ () ->
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.match(
+ "1",
+ Messages.start()
+ .text("Bye", "Emma")
+ .onFinish(Menu.gotoMenu("missing"))
+ .build()))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void rejectsUndefinedGotoTargetInRecordingHandlers() {
+ Assertions.assertThatThrownBy(
+ () ->
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.onFailure(
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .onFailure(Menu.gotoMenu("missing"))
+ .build()))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ Assertions.assertThatThrownBy(
+ () ->
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.onFailure(
+ Recording.start(RecordingDestinationType.AWS, "s3://b", "c")
+ .onFinish(Menu.gotoMenu("missing"))
+ .build()))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void rejectsHandBuiltNestedMenuWithUndefinedStartMenu() {
+ MenuCommand nested =
+ MenuCommand.builder()
+ .setStartMenu("other")
+ .setMenus(Collections.singletonMap("inner", MenuItem.builder().build()))
+ .build();
+
+ Assertions.assertThatThrownBy(
+ () -> Menu.create("main").item("main", item -> item.match("9", nested)).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"other\"");
+ }
+
+ @Test
+ void rejectsHandBuiltNestedMenuWithUndefinedGotoTarget() {
+ MenuItem inner =
+ MenuItem.builder().setOnFailure(Collections.singletonList(Menu.gotoMenu("main"))).build();
+ MenuCommand nested =
+ MenuCommand.builder()
+ .setStartMenu("inner")
+ .setMenus(Collections.singletonMap("inner", inner))
+ .build();
+
+ // "main" is an item of the outer menu, not of the nested one
+ Assertions.assertThatThrownBy(
+ () -> Menu.create("main").item("main", item -> item.match("9", nested)).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"main\"");
+ }
+
+ @Test
+ void rejectsUndefinedStartMenu() {
+ Assertions.assertThatThrownBy(() -> Menu.create("other").item("main", item -> {}).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"other\"");
+ }
+
+ @Test
+ void rejectsUndefinedGotoTarget() {
+ Assertions.assertThatThrownBy(
+ () ->
+ Menu.create("main")
+ .item("main", item -> item.match("1", Menu.gotoMenu("missing")))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void rejectsUndefinedGotoTargetInNestedHandler() {
+ Assertions.assertThatThrownBy(
+ () ->
+ Menu.create("main")
+ .item(
+ "main",
+ item ->
+ item.onFailure(
+ Calls.dial("+15551234567")
+ .onBusy(
+ Amd.activate().onMachine(Menu.gotoMenu("missing")).build())
+ .build()))
+ .build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("\"missing\"");
+ }
+
+ @Test
+ void nestedMenuKeepsItsOwnItems() {
+ MenuCommand nested =
+ Menu.create("inner").item("inner", item -> item.match("1", Menu.gotoMenu("inner"))).build();
+
+ Assertions.assertThatCode(
+ () -> Menu.create("main").item("main", item -> item.match("9", nested)).build())
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void promptAcceptsAVoiceNameFromTheCatalog() throws JsonProcessingException, JSONException {
+ assertJson(
+ Menu.prompt(
+ prompt ->
+ prompt.text("Press 1", VoiceName.EMMA).ssml("1", VoiceName.EMMA)),
+ objectMapper.writeValueAsString(
+ Menu.prompt(
+ prompt -> prompt.text("Press 1", "Emma").ssml("1", "Emma"))));
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> Menu.create(null)).isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Menu.gotoMenu(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Menu.prompt(null)).isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Menu.create("main").item(null, item -> {}))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(
+ () -> Menu.create("main").item("main", item -> item.match(null, Calls.hangup())))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesTest.java
new file mode 100644
index 000000000..db09fa53b
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/MessagesTest.java
@@ -0,0 +1,141 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.VoiceName;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class MessagesTest extends HelpersBaseTest {
+
+ static final String SAY_JSON =
+ "{\"type\":\"SAY\",\"say\":{\"text\":\"Hello\",\"voiceName\":\"Emma\"}}";
+ static final String PLAY_JSON =
+ "{\"type\":\"PLAY\",\"play\":{\"url\":\"https://example.com/a.mp3\"}}";
+
+ @Test
+ void textLeavesFormatUnset() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.text("Hello", "Emma"),
+ "{\"command\":\"messages\",\"messages\":[" + SAY_JSON + "]}");
+ }
+
+ @Test
+ void ssml() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.ssml("Hello", "Emma"),
+ "{\"command\":\"messages\",\"messages\":[{\"type\":\"SAY\",\"say\":"
+ + "{\"text\":\"Hello\",\"voiceName\":\"Emma\",\"format\":\"SSML\"}}]}");
+ }
+
+ @Test
+ void play() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.play("https://example.com/a.mp3"),
+ "{\"command\":\"messages\",\"messages\":[" + PLAY_JSON + "]}");
+ }
+
+ @Test
+ void startKeepsMessagesInOrder() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.start()
+ .text("Hello", "Emma")
+ .play("https://example.com/a.mp3")
+ .name("greeting")
+ .onFinish(Calls.hangup())
+ .build(),
+ "{\"command\":\"messages\",\"messagesName\":\"greeting\",\"messages\":["
+ + SAY_JSON
+ + ","
+ + PLAY_JSON
+ + "],\"events\":{\"onFinish\":["
+ + HANGUP_JSON
+ + "]}}");
+ }
+
+ @Test
+ void startRequiresAMessage() {
+ Assertions.assertThatThrownBy(() -> Messages.start().name("greeting").build())
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void stop() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.stop("greeting"), "{\"command\":\"stopMessages\",\"messagesName\":\"greeting\"}");
+ }
+
+ @Test
+ void shortcutsAcceptNameAndOnFinish() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.text("Hello", "Emma", m -> m.name("greeting").onFinish(Calls.hangup())),
+ "{\"command\":\"messages\",\"messagesName\":\"greeting\",\"messages\":["
+ + SAY_JSON
+ + "],\"events\":{\"onFinish\":["
+ + HANGUP_JSON
+ + "]}}");
+ assertJson(
+ Messages.ssml("Hello", "Emma", m -> m.name("greeting")),
+ "{\"command\":\"messages\",\"messagesName\":\"greeting\",\"messages\":[{\"type\":\"SAY\","
+ + "\"say\":{\"text\":\"Hello\",\"voiceName\":\"Emma\","
+ + "\"format\":\"SSML\"}}]}");
+ assertJson(
+ Messages.play("https://example.com/a.mp3", m -> m.onFinish(s -> s.hangup())),
+ "{\"command\":\"messages\",\"messages\":["
+ + PLAY_JSON
+ + "],\"events\":{\"onFinish\":["
+ + HANGUP_JSON
+ + "]}}");
+ }
+
+ @Test
+ void shortcutLambdaCanAddMessages() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.text("Hello", "Emma", m -> m.play("https://example.com/a.mp3")),
+ "{\"command\":\"messages\",\"messages\":[" + SAY_JSON + "," + PLAY_JSON + "]}");
+ }
+
+ @Test
+ void rejectsNullShortcutLambda() {
+ Assertions.assertThatThrownBy(() -> Messages.text("Hello", "Emma", null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Messages.play("https://example.com/a.mp3", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void acceptsAVoiceNameFromTheCatalog() throws JsonProcessingException, JSONException {
+ assertJson(
+ Messages.text("Hello", VoiceName.EMMA),
+ objectMapper.writeValueAsString(Messages.text("Hello", "Emma")));
+ assertJson(
+ Messages.ssml("Hello", VoiceName.EMMA, m -> m.name("greeting")),
+ objectMapper.writeValueAsString(
+ Messages.ssml("Hello", "Emma", m -> m.name("greeting"))));
+ assertJson(
+ Messages.start()
+ .text("Hello", VoiceName.EMMA)
+ .ssml("Hi", VoiceName.EMMA_3)
+ .build(),
+ objectMapper.writeValueAsString(
+ Messages.start().text("Hello", "Emma").ssml("Hi", "Emma_3").build()));
+ }
+
+ @Test
+ void rejectsNullVoiceName() {
+ Assertions.assertThatThrownBy(() -> Messages.text("Hello", (VoiceName) null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> Messages.text(null, "Emma"))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Messages.text("Hello", (String) null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Messages.play(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Messages.stop(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingTest.java
new file mode 100644
index 000000000..b17828f0f
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/RecordingTest.java
@@ -0,0 +1,213 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingDestinationType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingFormatType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingOptions;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.RecordingType;
+import com.sinch.sdk.domains.voice.models.v2.svaml.recording.TranscriptionOptions;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class RecordingTest extends HelpersBaseTest {
+
+ static final String OPTIONS_JSON =
+ "{\"destination\":\"AWS\",\"destinationUrl\":\"s3://voice-recordings/support\","
+ + "\"credentials\":\"key:secret:eu-west-1\"}";
+
+ static RecordingOptions awsOptions() {
+ return Recording.options(
+ RecordingDestinationType.AWS, "s3://voice-recordings/support", "key:secret:eu-west-1");
+ }
+
+ @Test
+ void options() throws JsonProcessingException, JSONException {
+ assertJson(awsOptions(), OPTIONS_JSON);
+ }
+
+ @Test
+ void startWithDestinationOnlySetsRequiredOptions() throws JsonProcessingException, JSONException {
+ assertJson(
+ Recording.start(
+ RecordingDestinationType.AWS,
+ "s3://voice-recordings/support",
+ "key:secret:eu-west-1")
+ .build(),
+ "{\"command\":\"startRecording\",\"recordingOptions\":" + OPTIONS_JSON + "}");
+ }
+
+ @Test
+ void startWithDestinationAndOptionalOptions() throws JsonProcessingException, JSONException {
+ assertJson(
+ Recording.start(
+ RecordingDestinationType.AWS,
+ "s3://voice-recordings/support",
+ "key:secret:eu-west-1")
+ .format(RecordingFormatType.WAV)
+ .recordingType(RecordingType.INBOUND)
+ .transcriptionOptions(
+ TranscriptionOptions.builder().setIsEnabled(true).setLocale("es-ES").build())
+ .name("support-call")
+ .build(),
+ "{\"command\":\"startRecording\",\"recordingName\":\"support-call\","
+ + "\"recordingOptions\":{\"destination\":\"AWS\","
+ + "\"destinationUrl\":\"s3://voice-recordings/support\","
+ + "\"credentials\":\"key:secret:eu-west-1\",\"format\":\"WAV\","
+ + "\"recordingType\":\"INBOUND\","
+ + "\"transcriptionOptions\":{\"isEnabled\":true,\"locale\":\"es-ES\"}}}");
+ }
+
+ @Test
+ void creatorOptionsOverrideGivenOptions() throws JsonProcessingException, JSONException {
+ RecordingOptions options =
+ RecordingOptions.builder()
+ .setDestination(RecordingDestinationType.AZURE)
+ .setDestinationUrl("azure://container/path")
+ .setCredentials("creds")
+ .setFormat(RecordingFormatType.MP3)
+ .setRecordingType(RecordingType.OUTBOUND)
+ .build();
+
+ assertJson(
+ Recording.start(options).format(RecordingFormatType.WAV).build(),
+ "{\"command\":\"startRecording\",\"recordingOptions\":{\"destination\":\"AZURE\","
+ + "\"destinationUrl\":\"azure://container/path\",\"credentials\":\"creds\","
+ + "\"format\":\"WAV\",\"recordingType\":\"OUTBOUND\"}}");
+ }
+
+ @Test
+ void startOnlySetsOptions() throws JsonProcessingException, JSONException {
+ assertJson(
+ Recording.start(awsOptions()).build(),
+ "{\"command\":\"startRecording\",\"recordingOptions\":" + OPTIONS_JSON + "}");
+ }
+
+ @Test
+ void startAllFields() throws JsonProcessingException, JSONException {
+ assertJson(
+ Recording.start(awsOptions())
+ .name("support-call")
+ .onFinish(Calls.hangup())
+ .onFailure(Calls.hangup("origin"))
+ .build(),
+ "{\"command\":\"startRecording\",\"recordingName\":\"support-call\","
+ + "\"recordingOptions\":"
+ + OPTIONS_JSON
+ + ",\"events\":{\"onFinish\":["
+ + HANGUP_JSON
+ + "],\"onFailure\":[{\"command\":\"hangup\",\"callName\":\"origin\"}]}}");
+ }
+
+ @Test
+ void startAcceptsOptionsFromBuilder() throws JsonProcessingException, JSONException {
+ RecordingOptions options =
+ RecordingOptions.builder()
+ .setDestination(RecordingDestinationType.GCP)
+ .setDestinationUrl("gs://bucket/path")
+ .setCredentials("creds")
+ .setFormat(RecordingFormatType.WAV)
+ .build();
+
+ assertJson(
+ Recording.start(options).build(),
+ "{\"command\":\"startRecording\",\"recordingOptions\":{\"destination\":\"GCP\","
+ + "\"destinationUrl\":\"gs://bucket/path\",\"credentials\":\"creds\","
+ + "\"format\":\"WAV\"}}");
+ }
+
+ @Test
+ void stop() throws JsonProcessingException, JSONException {
+ assertJson(
+ Recording.stop("support-call"),
+ "{\"command\":\"stopRecording\",\"recordingName\":\"support-call\"}");
+ }
+
+ @Test
+ void givenOptionsAreSentUnchangedWithoutOverride() {
+ RecordingOptions options =
+ RecordingOptions.builder()
+ .setDestination(RecordingDestinationType.AWS)
+ .setDestinationUrl("s3://b")
+ .setCredentials("c")
+ .setTranscriptionOptions(TranscriptionOptions.builder().setIsEnabled(true).build())
+ .build();
+
+ Assertions.assertThat(Recording.start(options).name("rec").build().getRecordingOptions())
+ .isSameAs(options);
+ Assertions.assertThat(
+ Recording.start(options).format(RecordingFormatType.WAV).build().getRecordingOptions())
+ .isNotSameAs(options);
+ }
+
+ /**
+ * RecordingCreator copies every field of the given options when one is overridden: a new field in
+ * RecordingOptions must be copied too
+ */
+ @Test
+ void recordingOptionsFieldsAreAllCopied() {
+ Assertions.assertThat(RecordingOptions.class.getDeclaredMethods())
+ .filteredOn(method -> method.getName().startsWith("get"))
+ .extracting(java.lang.reflect.Method::getName)
+ .containsExactlyInAnyOrder(
+ "getFormat",
+ "getRecordingType",
+ "getDestination",
+ "getDestinationUrl",
+ "getCredentials",
+ "getTranscriptionOptions");
+ }
+
+ @Test
+ void optionsWithOptionalFields() throws JsonProcessingException, JSONException {
+ RecordingOptions options =
+ Recording.options(
+ RecordingDestinationType.AWS,
+ "s3://voice-recordings/support",
+ "key:secret:eu-west-1",
+ o ->
+ o.setFormat(RecordingFormatType.WAV)
+ .setRecordingType(RecordingType.INBOUND)
+ .setTranscriptionOptions(
+ TranscriptionOptions.builder().setIsEnabled(true).build()));
+
+ assertJson(
+ options,
+ "{\"destination\":\"AWS\",\"destinationUrl\":\"s3://voice-recordings/support\","
+ + "\"credentials\":\"key:secret:eu-west-1\",\"format\":\"WAV\","
+ + "\"recordingType\":\"INBOUND\",\"transcriptionOptions\":{\"isEnabled\":true}}");
+ }
+
+ @Test
+ void sharedOptionsWithOptionalFieldsAreSentUnchanged() {
+ RecordingOptions shared =
+ Recording.options(
+ RecordingDestinationType.AWS, "s3://b", "c", o -> o.setFormat(RecordingFormatType.WAV));
+
+ Assertions.assertThat(Recording.start(shared).name("first").build().getRecordingOptions())
+ .isSameAs(shared);
+ Assertions.assertThat(Recording.start(shared).name("second").build().getRecordingOptions())
+ .isSameAs(shared);
+ }
+
+ @Test
+ void rejectsNullOptionsLambda() {
+ Assertions.assertThatThrownBy(
+ () -> Recording.options(RecordingDestinationType.AWS, "s3://b", "c", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(() -> Recording.options(null, "s3://b", "c"))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Recording.start(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Recording.start(RecordingDestinationType.AWS, null, "c"))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Recording.start(awsOptions()).format(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> Recording.stop(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponsesTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponsesTest.java
new file mode 100644
index 000000000..0a43cd343
--- /dev/null
+++ b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/helpers/SinchEventResponsesTest.java
@@ -0,0 +1,142 @@
+package com.sinch.sdk.domains.voice.models.v2.helpers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse;
+import java.util.Collections;
+import org.assertj.core.api.Assertions;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+
+class SinchEventResponsesTest extends HelpersBaseTest {
+
+ @Test
+ void response() throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.response(Calls.answer(), Calls.hangup()),
+ "{\"commands\":[{\"command\":\"answer\"}," + HANGUP_JSON + "]}");
+ }
+
+ @Test
+ void responseFromCollectionAndLambda() throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.response(Collections.singletonList(Calls.hangup())),
+ "{\"commands\":[" + HANGUP_JSON + "]}");
+ assertJson(
+ SinchEventResponses.response(seq -> seq.text("Bye", "Emma").hangup()),
+ "{\"commands\":[{\"command\":\"messages\",\"messages\":[{\"type\":\"SAY\",\"say\":"
+ + "{\"text\":\"Bye\",\"voiceName\":\"Emma\"}}]},"
+ + HANGUP_JSON
+ + "]}");
+ }
+
+ @Test
+ void responseAcceptsNoCommand() throws JsonProcessingException, JSONException {
+ assertJson(SinchEventResponses.response(), "{\"commands\":[]}");
+ }
+
+ @Test
+ void incomingCallResponseOnlySetsCommands() throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.incomingCallResponse(Calls.answer()).build(),
+ "{\"commands\":[{\"command\":\"answer\"}]}");
+ }
+
+ @Test
+ void incomingCallResponseAllFields() throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.incomingCallResponse(seq -> seq.answer().bridgeCall("support"))
+ .name("customer")
+ .onHangup(Calls.hangup("agent"))
+ .onHangup(seq -> seq.stopRecording("rec"))
+ .build(),
+ "{\"commands\":[{\"command\":\"answer\"},"
+ + "{\"command\":\"bridgeCall\",\"bridgeName\":\"support\"}],"
+ + "\"callName\":\"customer\",\"events\":{\"onHangup\":["
+ + "{\"command\":\"hangup\",\"callName\":\"agent\"},"
+ + "{\"command\":\"stopRecording\",\"recordingName\":\"rec\"}]}}");
+ }
+
+ @Test
+ void incomingCallResponseFromCollection() throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.incomingCallResponse(Collections.singletonList(Calls.answer()))
+ .onHangup(Collections.singletonList(Calls.hangup()))
+ .build(),
+ "{\"commands\":[{\"command\":\"answer\"}],\"events\":{\"onHangup\":["
+ + HANGUP_JSON
+ + "]}}");
+ }
+
+ @Test
+ void responseAcceptsGotoMenu() throws JsonProcessingException, JSONException {
+ // the event may have been triggered within a menu, where gotoMenu is valid
+ assertJson(
+ SinchEventResponses.response(Menu.gotoMenu("main")),
+ "{\"commands\":[{\"command\":\"gotoMenu\",\"menuName\":\"main\"}]}");
+ }
+
+ @Test
+ void incomingCallResponseRejectsGotoMenuOutsideAMenu() {
+ Assertions.assertThatThrownBy(
+ () -> SinchEventResponses.incomingCallResponse(Menu.gotoMenu("main")).build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("only valid within a menu");
+ Assertions.assertThatThrownBy(
+ () ->
+ SinchEventResponses.incomingCallResponse(Calls.answer())
+ .onHangup(seq -> seq.gotoMenu("main"))
+ .build())
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void acceptsValidMenu() {
+ Assertions.assertThatCode(
+ () ->
+ SinchEventResponses.response(
+ seq ->
+ seq.menu(
+ "main",
+ menu ->
+ menu.item("main", item -> item.match("1", Menu.gotoMenu("main"))))))
+ .doesNotThrowAnyException();
+ Assertions.assertThatCode(
+ () ->
+ SinchEventResponses.incomingCallResponse(
+ Menu.create("main")
+ .item("main", item -> item.onFailure(Menu.gotoMenu("main")))
+ .build())
+ .build())
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ void incomingCallResponseKeepsExplicitlyEmptyOnHangup()
+ throws JsonProcessingException, JSONException {
+ assertJson(
+ SinchEventResponses.incomingCallResponse(Calls.answer()).onHangup().build(),
+ "{\"commands\":[{\"command\":\"answer\"}],\"events\":{\"onHangup\":[]}}");
+ }
+
+ @Test
+ void incomingCallResponseBuildIsNotAffectedByLaterChanges() {
+ IncomingCallResponseCreator creator =
+ SinchEventResponses.incomingCallResponse(Calls.answer()).onHangup(Calls.hangup());
+ VoiceSinchEventResponse first = creator.build();
+
+ creator.onHangup(Calls.pause(10)).name("later");
+
+ Assertions.assertThat(first.getEvents().getOnHangup()).hasSize(1);
+ Assertions.assertThat(first.getCallName()).isNull();
+ Assertions.assertThat(creator.build().getEvents().getOnHangup()).hasSize(2);
+ }
+
+ @Test
+ void rejectsNullValues() {
+ Assertions.assertThatThrownBy(
+ () -> SinchEventResponses.incomingCallResponse(Calls.answer()).name(null))
+ .isInstanceOf(NullPointerException.class);
+ Assertions.assertThatThrownBy(() -> SinchEventResponses.response(Calls.answer(), null))
+ .isInstanceOf(NullPointerException.class);
+ }
+}
diff --git a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java
index 2148b47b2..17162dc16 100644
--- a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java
+++ b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java
@@ -1,12 +1,9 @@
package com.mycompany.app.voice.v2;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
+import com.sinch.sdk.domains.voice.models.v2.helpers.SinchEventResponses;
import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent;
import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
-import java.util.Collections;
import java.util.logging.Logger;
import org.springframework.stereotype.Component;
@@ -16,31 +13,21 @@ public class ServerBusinessLogic {
private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName());
// An empty list of commands takes no action
- private static final VoiceSinchEventResponse NO_ACTION =
- VoiceSinchEventResponse.builder().setCommands(Collections.emptyList()).build();
+ private static final VoiceSinchEventResponse NO_ACTION = SinchEventResponses.response();
public VoiceSinchEventResponse incoming(VoiceSinchEvent event) {
LOGGER.info("Handle event: " + event);
// play a greeting, then hang up
- // "callName" and "events" only take effect in responses to "call.incoming" events
- return VoiceSinchEventResponse.builder()
- .setCallName("incoming")
- .setCommands(
- Collections.singletonList(
- MessagesCommand.builder()
- .setMessages(
- Collections.singletonList(
- SayMessage.builder()
- .setText("Thank you for calling. Goodbye.")
- .setVoiceName("Emma")
- .build()))
- .setEvents(
- MessageEvents.builder()
- .setOnFinish(Collections.singletonList(HangupCommand.builder().build()))
- .build())
- .build()))
+ // the call name only takes effect in responses to "call.incoming" events
+ return SinchEventResponses.incomingCallResponse(
+ commands ->
+ commands.text(
+ "Thank you for calling. Goodbye.",
+ "Emma",
+ goodbye -> goodbye.onFinish(Calls.hangup())))
+ .name("incoming")
.build();
}
diff --git a/examples/snippets/src/main/java/voice/v2/batches/Start.java b/examples/snippets/src/main/java/voice/v2/batches/Start.java
index 9c43c408b..a07b4297b 100644
--- a/examples/snippets/src/main/java/voice/v2/batches/Start.java
+++ b/examples/snippets/src/main/java/voice/v2/batches/Start.java
@@ -11,13 +11,8 @@
import com.sinch.sdk.domains.voice.api.v2.BatchesService;
import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest;
import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse;
-import com.sinch.sdk.domains.voice.models.v2.destination.Phone;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
+import com.sinch.sdk.domains.voice.models.v2.helpers.CommandsSequenceCreator;
import com.sinch.sdk.models.Configuration;
import java.util.Arrays;
import java.util.Collections;
@@ -56,34 +51,24 @@ public static void main(String[] args) {
BatchesService batchesService = client.voice().v2().batches();
- // Played to each recipient once their call is answered, then the call is hung up
- MessagesCommand greeting =
- MessagesCommand.builder()
- .setMessages(
- Collections.singletonList(
- SayMessage.builder()
- .setText("Hello, your call is now connected.")
- .setVoiceName("Emma")
- .build()))
- .setEvents(
- MessageEvents.builder()
- .setOnFinish(Collections.singletonList(HangupCommand.HANGUP_COMMAND))
- .build())
- .build();
-
+ // Dial each recipient: once the call is answered, play a greeting, then hang up.
// "@toNumber" is a placeholder, replaced in each call by that call's parameter value
- DialCommand dial =
- DialCommand.builder()
- .setCallName("Java_SDK_Snippet_Call")
- .setFrom(Phone.builder().setNumber(sinchPhoneNumber).build())
- .setTo(Phone.builder().setNumber("@toNumber").build())
- .setEvents(
- CallEvents.builder().setOnAnswer(Collections.singletonList(greeting)).build())
- .build();
-
StartBatchRequest request =
StartBatchRequest.builder()
- .setCommands(Collections.singletonList(dial))
+ .setCommands(
+ CommandsSequenceCreator.create()
+ .dial(
+ "@toNumber",
+ dial ->
+ dial.name("Java_SDK_Snippet_Call")
+ .from(sinchPhoneNumber)
+ .onAnswer(
+ answered ->
+ answered.text(
+ "Hello, your call is now connected.",
+ "Emma",
+ greeting -> greeting.onFinish(Calls.hangup()))))
+ .build())
.setParameters(parameters)
.build();
diff --git a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java
index 14716beee..1bc3e47ec 100644
--- a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java
+++ b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java
@@ -10,9 +10,8 @@
import com.sinch.sdk.SinchClient;
import com.sinch.sdk.domains.voice.api.v2.CallsService;
import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
import com.sinch.sdk.models.Configuration;
-import java.util.Collections;
import java.util.logging.Logger;
import utils.Settings;
@@ -40,10 +39,7 @@ public static void main(String[] args) {
CallsService callsService = client.voice().v2().calls();
- CallPatchRequest request =
- CallPatchRequest.builder()
- .setCommands(Collections.singletonList(HangupCommand.HANGUP_COMMAND))
- .build();
+ CallPatchRequest request = CallPatchRequest.builder().setCommands(Calls.hangup()).build();
LOGGER.info(String.format("Interact with call with ID '%s'", callId));
diff --git a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java
index 3208c2931..a77c5cfd2 100644
--- a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java
+++ b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java
@@ -10,9 +10,8 @@
import com.sinch.sdk.SinchClient;
import com.sinch.sdk.domains.voice.api.v2.CallsService;
import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
import com.sinch.sdk.models.Configuration;
-import java.util.Collections;
import java.util.logging.Logger;
import utils.Settings;
@@ -43,10 +42,7 @@ public static void main(String[] args) {
CallsService callsService = client.voice().v2().calls();
- CallPatchRequest request =
- CallPatchRequest.builder()
- .setCommands(Collections.singletonList(HangupCommand.HANGUP_COMMAND))
- .build();
+ CallPatchRequest request = CallPatchRequest.builder().setCommands(Calls.hangup()).build();
LOGGER.info(
String.format("Interact with call named '%s' in session '%s'", callName, sessionId));
diff --git a/examples/snippets/src/main/java/voice/v2/calls/Start.java b/examples/snippets/src/main/java/voice/v2/calls/Start.java
index 775928a00..00b4a3cb6 100644
--- a/examples/snippets/src/main/java/voice/v2/calls/Start.java
+++ b/examples/snippets/src/main/java/voice/v2/calls/Start.java
@@ -11,15 +11,9 @@
import com.sinch.sdk.domains.voice.api.v2.CallsService;
import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse;
-import com.sinch.sdk.domains.voice.models.v2.destination.Phone;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Calls;
+import com.sinch.sdk.domains.voice.models.v2.helpers.CommandsSequenceCreator;
import com.sinch.sdk.models.Configuration;
-import java.util.Collections;
import java.util.logging.Logger;
import utils.Settings;
@@ -49,33 +43,25 @@ public static void main(String[] args) {
CallsService callsService = client.voice().v2().calls();
- // Played to the recipient once the call is answered, then the call is hung up
- MessagesCommand greeting =
- MessagesCommand.builder()
- .setMessages(
- Collections.singletonList(
- SayMessage.builder()
- .setText("Hello, your call is now connected.")
- .setVoiceName("Emma")
- .build()))
- .setEvents(
- MessageEvents.builder()
- .setOnFinish(Collections.singletonList(HangupCommand.HANGUP_COMMAND))
+ // Dial the recipient: once the call is answered, play a greeting, then hang up
+ StartCallRequest request =
+ StartCallRequest.builder()
+ .setCommands(
+ CommandsSequenceCreator.create()
+ .dial(
+ recipientPhoneNumber,
+ dial ->
+ dial.name("Java_SDK_Snippet_Call")
+ .from(sinchPhoneNumber)
+ .onAnswer(
+ answered ->
+ answered.text(
+ "Hello, your call is now connected.",
+ "Emma",
+ greeting -> greeting.onFinish(Calls.hangup()))))
.build())
.build();
- DialCommand dial =
- DialCommand.builder()
- .setCallName("Java_SDK_Snippet_Call")
- .setFrom(Phone.builder().setNumber(sinchPhoneNumber).build())
- .setTo(Phone.builder().setNumber(recipientPhoneNumber).build())
- .setEvents(
- CallEvents.builder().setOnAnswer(Collections.singletonList(greeting)).build())
- .build();
-
- StartCallRequest request =
- StartCallRequest.builder().setCommands(Collections.singletonList(dial)).build();
-
LOGGER.info("Start a call to: " + recipientPhoneNumber);
StartCallResponse response = callsService.start(request);
diff --git a/examples/snippets/src/main/java/voice/v2/svaml/Describe.java b/examples/snippets/src/main/java/voice/v2/svaml/Describe.java
index 647173dbe..95cd3d0e1 100644
--- a/examples/snippets/src/main/java/voice/v2/svaml/Describe.java
+++ b/examples/snippets/src/main/java/voice/v2/svaml/Describe.java
@@ -9,13 +9,11 @@
import com.sinch.sdk.SinchClient;
import com.sinch.sdk.domains.voice.api.v2.SvamlService;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Messages;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest;
import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse;
import com.sinch.sdk.models.Configuration;
-import java.util.Collections;
import java.util.logging.Logger;
import utils.Settings;
@@ -44,16 +42,7 @@ public static void main(String[] args) {
DescribeSvamlRequest.builder()
.setSvaml(
SvamlInput.builder()
- .setCommands(
- Collections.singletonList(
- MessagesCommand.builder()
- .setMessages(
- Collections.singletonList(
- SayMessage.builder()
- .setText("Hello, your call is now connected.")
- .setVoiceName("Emma")
- .build()))
- .build()))
+ .setCommands(Messages.text("Hello, your call is now connected.", "Emma"))
.build())
.build();
diff --git a/examples/snippets/src/main/java/voice/v2/svaml/Validate.java b/examples/snippets/src/main/java/voice/v2/svaml/Validate.java
index 50f3279a0..2c4b21c75 100644
--- a/examples/snippets/src/main/java/voice/v2/svaml/Validate.java
+++ b/examples/snippets/src/main/java/voice/v2/svaml/Validate.java
@@ -9,13 +9,11 @@
import com.sinch.sdk.SinchClient;
import com.sinch.sdk.domains.voice.api.v2.SvamlService;
+import com.sinch.sdk.domains.voice.models.v2.helpers.Messages;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand;
-import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage;
import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest;
import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse;
import com.sinch.sdk.models.Configuration;
-import java.util.Collections;
import java.util.logging.Logger;
import utils.Settings;
@@ -44,16 +42,7 @@ public static void main(String[] args) {
ValidateSvamlRequest.builder()
.setSvaml(
SvamlInput.builder()
- .setCommands(
- Collections.singletonList(
- MessagesCommand.builder()
- .setMessages(
- Collections.singletonList(
- SayMessage.builder()
- .setText("Hello, your call is now connected.")
- .setVoiceName("Emma")
- .build()))
- .build()))
+ .setCommands(Messages.text("Hello, your call is now connected.", "Emma"))
.build())
.build();
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceName.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceName.java
new file mode 100644
index 000000000..1b2f1c189
--- /dev/null
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceName.java
@@ -0,0 +1,3125 @@
+package com.sinch.sdk.domains.voice.models.v2;
+
+import com.sinch.sdk.core.utils.EnumDynamic;
+import com.sinch.sdk.core.utils.EnumSupportDynamic;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+/**
+ * Sinch Voice Name to use for text-to-speech. Each voice is tied to a specific language variant,
+ * gender, and pricing model (Free/Standard or Paid/Generative). See the
+ * text-to-speech voices for details
+ */
+public class VoiceName extends EnumDynamic {
+
+ /** Male voice for Hindi (India) (Free voice) */
+ public static final VoiceName AARAV = new VoiceName("Aarav");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName AARAV_2 = new VoiceName("Aarav_2");
+
+ /** Female voice for Marathi (India) (Free voice) */
+ public static final VoiceName AAROHI = new VoiceName("Aarohi");
+
+ /** Female voice for Hindi (India) (Free voice) */
+ public static final VoiceName AARTI = new VoiceName("Aarti");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName AARTI_2 = new VoiceName("Aarti_2");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName AARTI_3 = new VoiceName("Aarti_3");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName AASHI = new VoiceName("Aashi");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ABBI = new VoiceName("Abbi");
+
+ /** Male voice for Arabic (Oman) (Free voice) */
+ public static final VoiceName ABDULLAH = new VoiceName("Abdullah");
+
+ /** Male voice for English (Nigeria) (Free voice) */
+ public static final VoiceName ABEO = new VoiceName("Abeo");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ABRIL = new VoiceName("Abril");
+
+ /** Female voice for Thai (Thailand) (Free voice) */
+ public static final VoiceName ACHARA = new VoiceName("Achara");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ADA = new VoiceName("Ada");
+
+ /** Female voice for English (United Kingdom) (Paid voice) */
+ public static final VoiceName ADA_2 = new VoiceName("Ada_2");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ADAM = new VoiceName("Adam");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName ADAM_2 = new VoiceName("Adam_2");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName ADITI = new VoiceName("Aditi");
+
+ /** Female voice for Afrikaans (South Africa) (Free voice) */
+ public static final VoiceName ADRI = new VoiceName("Adri");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName ADRIANO = new VoiceName("Adriano");
+
+ /** Female voice for Polish (Poland) (Free voice) */
+ public static final VoiceName AGNIESZKA = new VoiceName("Agnieszka");
+
+ /** Male voice for Turkish (Türkiye) (Free voice) */
+ public static final VoiceName AHMET = new VoiceName("Ahmet");
+
+ /** Female voice for Kazakh (Kazakhstan) (Free voice) */
+ public static final VoiceName AIGUL = new VoiceName("Aigul");
+
+ /** Female voice for Basque (Free voice) */
+ public static final VoiceName AINHOA = new VoiceName("Ainhoa");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName ALAIN = new VoiceName("Alain");
+
+ /** Female voice for Catalan (Free voice) */
+ public static final VoiceName ALBA = new VoiceName("Alba");
+
+ /** Male voice for Welsh (United Kingdom) (Free voice) */
+ public static final VoiceName ALED = new VoiceName("Aled");
+
+ /** Male voice for Macedonian (North Macedonia) (Free voice) */
+ public static final VoiceName ALEKSANDAR = new VoiceName("Aleksandar");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName ALESSIO = new VoiceName("Alessio");
+
+ /** Male voice for Italian (Italy) (Paid voice) */
+ public static final VoiceName ALESSIO_2 = new VoiceName("Alessio_2");
+
+ /** Male voice for Spanish (Peru) (Free voice) */
+ public static final VoiceName ALEX = new VoiceName("Alex");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ALFIE = new VoiceName("Alfie");
+
+ /** Male voice for Arabic (Bahrain) (Free voice) */
+ public static final VoiceName ALI = new VoiceName("Ali");
+
+ /** Female voice for Romanian (Romania) (Free voice) */
+ public static final VoiceName ALINA = new VoiceName("Alina");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ALLOY = new VoiceName("Alloy");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName ALLOY_2 = new VoiceName("Alloy_2");
+
+ /** Male voice for Spanish (United States) (Free voice) */
+ public static final VoiceName ALONSO = new VoiceName("Alonso");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ALVARO = new VoiceName("Alvaro");
+
+ /** Female voice for Arabic (Qatar) (Free voice) */
+ public static final VoiceName AMAL = new VoiceName("Amal");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName AMALA = new VoiceName("Amala");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName AMANDA = new VoiceName("Amanda");
+
+ /** Female voice for Arabic (Syria) (Free voice) */
+ public static final VoiceName AMANY = new VoiceName("Amany");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName AMBER = new VoiceName("Amber");
+
+ /** Female voice for French (France) (Paid voice) */
+ public static final VoiceName AMBRE = new VoiceName("Ambre");
+
+ /** Male voice for Amharic (Ethiopia) (Free voice) */
+ public static final VoiceName AMEHA = new VoiceName("Ameha");
+
+ /** Female voice for Arabic (Algeria) (Free voice) */
+ public static final VoiceName AMINA = new VoiceName("Amina");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName AMY = new VoiceName("Amy");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName AMY_2 = new VoiceName("Amy_2");
+
+ /** Female voice for English (United Kingdom) (Paid voice) */
+ public static final VoiceName AMY_3 = new VoiceName("Amy_3");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName ANA = new VoiceName("Ana");
+
+ /** Female voice for Armenian (Armenia) (Free voice) */
+ public static final VoiceName ANAHIT = new VoiceName("Anahit");
+
+ /** Female voice for Hindi (India) (Free voice) */
+ public static final VoiceName ANANYA = new VoiceName("Ananya");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName ANANYA_2 = new VoiceName("Ananya_2");
+
+ /** Male voice for Tamil (Singapore) (Free voice) */
+ public static final VoiceName ANBU = new VoiceName("Anbu");
+
+ /** Male voice for Basque (Free voice) */
+ public static final VoiceName ANDER = new VoiceName("Ander");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName ANDRE = new VoiceName("Andre");
+
+ /** Male voice for Spanish (Mexico) (Paid voice) */
+ public static final VoiceName ANDRE_2 = new VoiceName("Andre_2");
+
+ /** Female voice for Spanish (Ecuador) (Free voice) */
+ public static final VoiceName ANDREA = new VoiceName("Andrea");
+
+ /** Male voice for Spanish (Guatemala) (Free voice) */
+ public static final VoiceName ANDRES = new VoiceName("Andres");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ANDREW = new VoiceName("Andrew");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ANDREW_2 = new VoiceName("Andrew_2");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName ANDREW_3 = new VoiceName("Andrew_3");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName ANDREW_4 = new VoiceName("Andrew_4");
+
+ /** Male voice for Filipino (Philippines) (Free voice) */
+ public static final VoiceName ANGELO = new VoiceName("Angelo");
+
+ /** Female voice for Albanian (Albania) (Free voice) */
+ public static final VoiceName ANILA = new VoiceName("Anila");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName ANNETTE = new VoiceName("Annette");
+
+ /** Male voice for French (Canada) (Free voice) */
+ public static final VoiceName ANTOINE = new VoiceName("Antoine");
+
+ /** Male voice for Czech (Czechia) (Free voice) */
+ public static final VoiceName ANTONIN = new VoiceName("Antonin");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName ANTONIO = new VoiceName("Antonio");
+
+ /** Female voice for Estonian (Estonia) (Free voice) */
+ public static final VoiceName ANU = new VoiceName("Anu");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName AOI = new VoiceName("Aoi");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ARABELLA = new VoiceName("Arabella");
+
+ /** Male voice for Indonesian (Indonesia) (Free voice) */
+ public static final VoiceName ARDI = new VoiceName("Ardi");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName ARIA = new VoiceName("Aria");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName ARIA_2 = new VoiceName("Aria_2");
+
+ /** Female voice for French (Switzerland) (Free voice) */
+ public static final VoiceName ARIANE = new VoiceName("Ariane");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName ARJUN = new VoiceName("Arjun");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName ARJUN_2 = new VoiceName("Arjun_2");
+
+ /** Male voice for Hindi (India) (Free voice) */
+ public static final VoiceName ARJUN_3 = new VoiceName("Arjun_3");
+
+ /** Female voice for Catalan (Free voice) */
+ public static final VoiceName ARLET = new VoiceName("Arlet");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ARNAU = new VoiceName("Arnau");
+
+ /** Male voice for Dutch (Belgium) (Free voice) */
+ public static final VoiceName ARNAUD = new VoiceName("Arnaud");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ARTHUR = new VoiceName("Arthur");
+
+ /** Female voice for English (New Zealand) (Free voice) */
+ public static final VoiceName ARYA = new VoiceName("Arya");
+
+ /** Female voice for English (New Zealand) (Paid voice) */
+ public static final VoiceName ARYA_2 = new VoiceName("Arya_2");
+
+ /** Male voice for Urdu (Pakistan) (Free voice) */
+ public static final VoiceName ASAD = new VoiceName("Asad");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName ASHLEY = new VoiceName("Ashley");
+
+ /** Female voice for English (Kenya) (Free voice) */
+ public static final VoiceName ASILIA = new VoiceName("Asilia");
+
+ /** Female voice for Swedish (Sweden) (Free voice) */
+ public static final VoiceName ASTRID = new VoiceName("Astrid");
+
+ /** Female voice for Greek (Greece) (Free voice) */
+ public static final VoiceName ATHINA = new VoiceName("Athina");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName AVA = new VoiceName("Ava");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName AVA_2 = new VoiceName("Ava_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName AVA_3 = new VoiceName("Ava_3");
+
+ /** Male voice for Hebrew (Israel) (Free voice) */
+ public static final VoiceName AVRI = new VoiceName("Avri");
+
+ /** Female voice for English (South Africa) (Free voice) */
+ public static final VoiceName AYANDA = new VoiceName("Ayanda");
+
+ /** Female voice for English (South Africa) (Paid voice) */
+ public static final VoiceName AYANDA_2 = new VoiceName("Ayanda_2");
+
+ /** Female voice for Arabic (Oman) (Free voice) */
+ public static final VoiceName AYSHA = new VoiceName("Aysha");
+
+ /** Male voice for Azerbaijani (Latin, Azerbaijan) (Free voice) */
+ public static final VoiceName BABEK = new VoiceName("Babek");
+
+ /** Female voice for Azerbaijani (Latin, Azerbaijan) (Free voice) */
+ public static final VoiceName BANU = new VoiceName("Banu");
+
+ /** Male voice for Bengali (India) (Free voice) */
+ public static final VoiceName BASHKAR = new VoiceName("Bashkar");
+
+ /** Male voice for Arabic (Iraq) (Free voice) */
+ public static final VoiceName BASSEL = new VoiceName("Bassel");
+
+ /** Male voice for Mongolian (Mongolia) (Free voice) */
+ public static final VoiceName BATAA = new VoiceName("Bataa");
+
+ /** Female voice for Italian (Italy) (Paid voice) */
+ public static final VoiceName BEATRICE = new VoiceName("Beatrice");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName BEATRIZ = new VoiceName("Beatriz");
+
+ /** Female voice for Spanish (Cuba) (Free voice) */
+ public static final VoiceName BELKYS = new VoiceName("Belkys");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName BELLA = new VoiceName("Bella");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName BENIGNO = new VoiceName("Benigno");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName BERND = new VoiceName("Bernd");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName BIANCA = new VoiceName("Bianca");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName BIANCA_2 = new VoiceName("Bianca_2");
+
+ /** Female voice for Italian (Italy) (Paid voice) */
+ public static final VoiceName BIANCA_3 = new VoiceName("Bianca_3");
+
+ /** Female voice for Filipino (Philippines) (Free voice) */
+ public static final VoiceName BLESSICA = new VoiceName("Blessica");
+
+ /** Male voice for Korean (Korea) (Free voice) */
+ public static final VoiceName BONG_JIN = new VoiceName("BongJin");
+
+ /** Male voice for Bulgarian (Bulgaria) (Free voice) */
+ public static final VoiceName BORISLAV = new VoiceName("Borislav");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName BRANDON = new VoiceName("Brandon");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName BRANDON_2 = new VoiceName("Brandon_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName BREE = new VoiceName("Bree");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName BRENDA = new VoiceName("Brenda");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName BRIAN = new VoiceName("Brian");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName BRIAN_2 = new VoiceName("Brian_2");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName BRIAN_3 = new VoiceName("Brian_3");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName BRIGITTE = new VoiceName("Brigitte");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName BRYAN = new VoiceName("Bryan");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName BRYAN_2 = new VoiceName("Bryan_2");
+
+ /** Male voice for English (United Kingdom) (Paid voice) */
+ public static final VoiceName BRYAN_3 = new VoiceName("Bryan_3");
+
+ /** Female voice for Turkish (Türkiye) (Free voice) */
+ public static final VoiceName BURCU = new VoiceName("Burcu");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName CALIMERO = new VoiceName("Calimero");
+
+ /** Female voice for Spanish (Peru) (Free voice) */
+ public static final VoiceName CAMILA = new VoiceName("Camila");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName CAMILLA = new VoiceName("Camilla");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName CAMILLA_2 = new VoiceName("Camilla_2");
+
+ /** Female voice for Portuguese (Brazil) (Paid voice) */
+ public static final VoiceName CAMILLA_3 = new VoiceName("Camilla_3");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName CANDELA = new VoiceName("Candela");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName CARLA = new VoiceName("Carla");
+
+ /** Male voice for Spanish (Honduras) (Free voice) */
+ public static final VoiceName CARLOS = new VoiceName("Carlos");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName CARLOTA = new VoiceName("Carlota");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName CARLY = new VoiceName("Carly");
+
+ /** Female voice for Romanian (Romania) (Free voice) */
+ public static final VoiceName CARMEN = new VoiceName("Carmen");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName CATALDO = new VoiceName("Cataldo");
+
+ /** Female voice for Spanish (Chile) (Free voice) */
+ public static final VoiceName CATALINA = new VoiceName("Catalina");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName CECILIO = new VoiceName("Cecilio");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName CELESTE = new VoiceName("Celeste");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName CELINE = new VoiceName("Celine");
+
+ /** Female voice for French (France) (Paid voice) */
+ public static final VoiceName CELINE_2 = new VoiceName("Celine_2");
+
+ /** Female voice for French (Canada) (Free voice) */
+ public static final VoiceName CHANTAL = new VoiceName("Chantal");
+
+ /** Male voice for Lao (Laos) (Free voice) */
+ public static final VoiceName CHANTHAVONG = new VoiceName("Chanthavong");
+
+ /** Female voice for French (Belgium) (Free voice) */
+ public static final VoiceName CHARLINE = new VoiceName("Charline");
+
+ /** Male voice for English (Kenya) (Free voice) */
+ public static final VoiceName CHILEMBA = new VoiceName("Chilemba");
+
+ /** Female voice for Danish (Denmark) (Free voice) */
+ public static final VoiceName CHRISTEL = new VoiceName("Christel");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName CHRISTOPH = new VoiceName("Christoph");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName CHRISTOPHER = new VoiceName("Christopher");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName CHRISTOPHER_2 = new VoiceName("Christopher_2");
+
+ /** Female voice for English (Canada) (Free voice) */
+ public static final VoiceName CLARA = new VoiceName("Clara");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName CLAUDE = new VoiceName("Claude");
+
+ /** Female voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName COLETTE = new VoiceName("Colette");
+
+ /** Male voice for Irish (Ireland) (Free voice) */
+ public static final VoiceName COLM = new VoiceName("Colm");
+
+ /** Female voice for Spanish (Castilian) (Free voice) */
+ public static final VoiceName CONCHITA = new VoiceName("Conchita");
+
+ /** Male voice for English (Ireland) (Free voice) */
+ public static final VoiceName CONNOR = new VoiceName("Connor");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName CONRAD = new VoiceName("Conrad");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName CORA = new VoiceName("Cora");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName CORA_2 = new VoiceName("Cora_2");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName CORALIE = new VoiceName("Coralie");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName CRISTIANO = new VoiceName("Cristiano");
+
+ /** Male voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName DAICHI = new VoiceName("Daichi");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName DALIA = new VoiceName("Dalia");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName DALIA_2 = new VoiceName("Dalia_2");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName DANIEL = new VoiceName("Daniel");
+
+ /** Male voice for German (Germany) (Paid voice) */
+ public static final VoiceName DANIEL_2 = new VoiceName("Daniel_2");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName DANIELLE = new VoiceName("Danielle");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName DANIELLE_2 = new VoiceName("Danielle_2");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName DARIO = new VoiceName("Dario");
+
+ /** Female voice for Russian (Russia) (Free voice) */
+ public static final VoiceName DARIYA = new VoiceName("Dariya");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName DARREN = new VoiceName("Darren");
+
+ /** Male voice for Swahili (Tanzania) (Free voice) */
+ public static final VoiceName DAUDI = new VoiceName("Daudi");
+
+ /** Male voice for Kazakh (Kazakhstan) (Free voice) */
+ public static final VoiceName DAULET = new VoiceName("Daulet");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName DAVIS = new VoiceName("Davis");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName DAVIS_2 = new VoiceName("Davis_2");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName DAVIS_3 = new VoiceName("Davis_3");
+
+ /** Female voice for Dutch (Belgium) (Free voice) */
+ public static final VoiceName DENA = new VoiceName("Dena");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName DENISE = new VoiceName("Denise");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName DEREK = new VoiceName("Derek");
+
+ /** Female voice for Gujarati (India) (Free voice) */
+ public static final VoiceName DHWANI = new VoiceName("Dhwani");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName DIEGO = new VoiceName("Diego");
+
+ /** Female voice for Persian (Iran) (Free voice) */
+ public static final VoiceName DILARA = new VoiceName("Dilara");
+
+ /** Male voice for Javanese (Latin, Indonesia) (Free voice) */
+ public static final VoiceName DIMAS = new VoiceName("Dimas");
+
+ /** Male voice for Russian (Russia) (Free voice) */
+ public static final VoiceName DMITRY = new VoiceName("Dmitry");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName DONATO = new VoiceName("Donato");
+
+ /** Female voice for Icelandic (Iceland) (Free voice) */
+ public static final VoiceName DORA = new VoiceName("Dora");
+
+ /** Male voice for Portuguese (Portugal) (Free voice) */
+ public static final VoiceName DUARTE = new VoiceName("Duarte");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName DUNCAN = new VoiceName("Duncan");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName DUSTIN = new VoiceName("Dustin");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ECHO = new VoiceName("Echo");
+
+ /** Female voice for Georgian (Georgia) (Free voice) */
+ public static final VoiceName EKA = new VoiceName("Eka");
+
+ /** Female voice for Spanish (Argentina) (Free voice) */
+ public static final VoiceName ELENA = new VoiceName("Elena");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ELIAS = new VoiceName("Elias");
+
+ /** Male voice for English (Tanzania) (Free voice) */
+ public static final VoiceName ELIMU = new VoiceName("Elimu");
+
+ /** Female voice for Swedish (Sweden) (Free voice) */
+ public static final VoiceName ELIN = new VoiceName("Elin");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName ELIZABETH = new VoiceName("Elizabeth");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName ELKE = new VoiceName("Elke");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ELLIOT = new VoiceName("Elliot");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName ELOISE = new VoiceName("Eloise");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName ELSA = new VoiceName("Elsa");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName ELSIE = new VoiceName("Elsie");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ELVIRA = new VoiceName("Elvira");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName ELZA = new VoiceName("Elza");
+
+ /** Female voice for Turkish (Türkiye) (Free voice) */
+ public static final VoiceName EMEL = new VoiceName("Emel");
+
+ /** Male voice for Romanian (Romania) (Free voice) */
+ public static final VoiceName EMIL = new VoiceName("Emil");
+
+ /** Male voice for Spanish (Dominican Republic) (Free voice) */
+ public static final VoiceName EMILIO = new VoiceName("Emilio");
+
+ /** Female voice for English (Ireland) (Free voice) */
+ public static final VoiceName EMILY = new VoiceName("Emily");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName EMMA = new VoiceName("Emma");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName EMMA_2 = new VoiceName("Emma_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName EMMA_3 = new VoiceName("Emma_3");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName EMMA_4 = new VoiceName("Emma_4");
+
+ /** Male voice for Catalan (Free voice) */
+ public static final VoiceName ENRIC = new VoiceName("Enric");
+
+ /** Male voice for Spanish (Castilian) (Free voice) */
+ public static final VoiceName ENRIQUE = new VoiceName("Enrique");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ERIC = new VoiceName("Eric");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ESTRELLA = new VoiceName("Estrella");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName ETHAN = new VoiceName("Ethan");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName EVELYN = new VoiceName("Evelyn");
+
+ /** Female voice for Latvian (Latvia) (Free voice) */
+ public static final VoiceName EVERITA = new VoiceName("Everita");
+
+ /** Female voice for Polish (Poland) (Free voice) */
+ public static final VoiceName EWA = new VoiceName("Ewa");
+
+ /** Female voice for Polish (Poland) (Paid voice) */
+ public static final VoiceName EWA_2 = new VoiceName("Ewa_2");
+
+ /** Female voice for English (Nigeria) (Free voice) */
+ public static final VoiceName EZINNE = new VoiceName("Ezinne");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName FABIO = new VoiceName("Fabio");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName FABIOLA = new VoiceName("Fabiola");
+
+ /** Neutral voice for English (United States) (Free voice) */
+ public static final VoiceName FABLE = new VoiceName("Fable");
+
+ /** Male voice for French (Switzerland) (Free voice) */
+ public static final VoiceName FABRICE = new VoiceName("Fabrice");
+
+ /** Male voice for Arabic (Kuwait) (Free voice) */
+ public static final VoiceName FAHED = new VoiceName("Fahed");
+
+ /** Male voice for Persian (Iran) (Free voice) */
+ public static final VoiceName FARID = new VoiceName("Farid");
+
+ /** Female voice for Arabic (United Arab Emirates) (Free voice) */
+ public static final VoiceName FATIMA = new VoiceName("Fatima");
+
+ /** Male voice for Spanish (Nicaragua) (Free voice) */
+ public static final VoiceName FEDERICO = new VoiceName("Federico");
+
+ /** Female voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName FENNA = new VoiceName("Fenna");
+
+ /** Female voice for Portuguese (Portugal) (Free voice) */
+ public static final VoiceName FERNANDA = new VoiceName("Fernanda");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName FIAMMA = new VoiceName("Fiamma");
+
+ /** Female voice for Turkish (Türkiye) (Free voice) */
+ public static final VoiceName FILIZ = new VoiceName("Filiz");
+
+ /** Male voice for Norwegian (Norway) (Free voice) */
+ public static final VoiceName FINN = new VoiceName("Finn");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName FLORIAN = new VoiceName("Florian");
+
+ /** Male voice for German (Germany) (Paid voice) */
+ public static final VoiceName FLORIAN_2 = new VoiceName("Florian_2");
+
+ /** Male voice for French (France) (Paid voice) */
+ public static final VoiceName FLORIN = new VoiceName("Florin");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName FRANCISCA = new VoiceName("Francisca");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName FREYA = new VoiceName("Freya");
+
+ /** Female voice for French (Canada) (Free voice) */
+ public static final VoiceName GABRIELLE = new VoiceName("Gabrielle");
+
+ /** Female voice for French (Canada) (Paid voice) */
+ public static final VoiceName GABRIELLE_2 = new VoiceName("Gabrielle_2");
+
+ /** Female voice for Croatian (Croatia) (Free voice) */
+ public static final VoiceName GABRIJELA = new VoiceName("Gabrijela");
+
+ /** Female voice for Indonesian (Indonesia) (Free voice) */
+ public static final VoiceName GADIS = new VoiceName("Gadis");
+
+ /** Male voice for Kannada (India) (Free voice) */
+ public static final VoiceName GAGAN = new VoiceName("Gagan");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName GEMMA = new VoiceName("Gemma");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName GEMMA_2 = new VoiceName("Gemma_2");
+
+ /** Male voice for Welsh (United Kingdom) (Free voice) */
+ public static final VoiceName GERAINT = new VoiceName("Geraint");
+
+ /** Male voice for French (Belgium) (Free voice) */
+ public static final VoiceName GERARD = new VoiceName("Gerard");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName GERARDO = new VoiceName("Gerardo");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName GIANNI = new VoiceName("Gianni");
+
+ /** Male voice for Georgian (Georgia) (Free voice) */
+ public static final VoiceName GIORGI = new VoiceName("Giorgi");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName GIORGIO = new VoiceName("Giorgio");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName GIOVANNA = new VoiceName("Giovanna");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName GISELA = new VoiceName("Gisela");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName GIUSEPPE = new VoiceName("Giuseppe");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName GIUSEPPE_2 = new VoiceName("Giuseppe_2");
+
+ /** Male voice for Spanish (Colombia) (Free voice) */
+ public static final VoiceName GONZALO = new VoiceName("Gonzalo");
+
+ /** Male voice for Korean (Korea) (Free voice) */
+ public static final VoiceName GOOK_MIN = new VoiceName("GookMin");
+
+ /** Male voice for Bosnian (Bosnia and Herzegovina) (Free voice) */
+ public static final VoiceName GORAN = new VoiceName("Goran");
+
+ /** Female voice for Maltese (Malta) (Free voice) */
+ public static final VoiceName GRACE = new VoiceName("Grace");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName GREGORY = new VoiceName("Gregory");
+
+ /** Female voice for Icelandic (Iceland) (Free voice) */
+ public static final VoiceName GUDRUN = new VoiceName("Gudrun");
+
+ /** Female voice for Urdu (India) (Free voice) */
+ public static final VoiceName GUL = new VoiceName("Gul");
+
+ /** Male voice for Pashto (Afghanistan) (Free voice) */
+ public static final VoiceName GUL_NAWAZ = new VoiceName("GulNawaz");
+
+ /** Male voice for Icelandic (Iceland) (Free voice) */
+ public static final VoiceName GUNNAR = new VoiceName("Gunnar");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName GUY = new VoiceName("Guy");
+
+ /** Female voice for Welsh (United Kingdom) (Free voice) */
+ public static final VoiceName GWYNETH = new VoiceName("Gwyneth");
+
+ /** Female voice for Arabic (Saudi Arabia) (Free voice) */
+ public static final VoiceName HALA = new VoiceName("Hala");
+
+ /** Male voice for Arabic (United Arab Emirates) (Free voice) */
+ public static final VoiceName HAMDAN = new VoiceName("Hamdan");
+
+ /** Male voice for Arabic (Saudi Arabia) (Free voice) */
+ public static final VoiceName HAMED = new VoiceName("Hamed");
+
+ /** Female voice for German (Austria) (Free voice) */
+ public static final VoiceName HANNAH = new VoiceName("Hannah");
+
+ /** Female voice for German (Austria) (Paid voice) */
+ public static final VoiceName HANNAH_2 = new VoiceName("Hannah_2");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName HANS = new VoiceName("Hans");
+
+ /** Male voice for Finnish (Finland) (Free voice) */
+ public static final VoiceName HARRI = new VoiceName("Harri");
+
+ /** Male voice for Armenian (Armenia) (Free voice) */
+ public static final VoiceName HAYK = new VoiceName("Hayk");
+
+ /** Male voice for Arabic (Tunisia) (Free voice) */
+ public static final VoiceName HEDI = new VoiceName("Hedi");
+
+ /** Female voice for Nepali (Nepal) (Free voice) */
+ public static final VoiceName HEMKALA = new VoiceName("Hemkala");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName HENRI = new VoiceName("Henri");
+
+ /** Female voice for Hebrew (Israel) (Free voice) */
+ public static final VoiceName HILA = new VoiceName("Hila");
+
+ /** Female voice for Swedish (Sweden) (Free voice) */
+ public static final VoiceName HILLEVI = new VoiceName("Hillevi");
+
+ /** Female voice for Chinese (Cantonese, Traditional) (Free voice) */
+ public static final VoiceName HIU_GAAI = new VoiceName("HiuGaai");
+
+ /** Female voice for Chinese (Cantonese) (Free voice) */
+ public static final VoiceName HIUJIN = new VoiceName("Hiujin");
+
+ /** Female voice for Chinese (Cantonese, Traditional) (Free voice) */
+ public static final VoiceName HIU_MAAN = new VoiceName("HiuMaan");
+
+ /** Female voice for Vietnamese (Vietnam) (Free voice) */
+ public static final VoiceName HOAI_MY = new VoiceName("HoaiMy");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName HOLLIE = new VoiceName("Hollie");
+
+ /** Female voice for Chinese (Taiwanese Mandarin, Traditional) (Free voice) */
+ public static final VoiceName HSIAO_CHEN = new VoiceName("HsiaoChen");
+
+ /** Female voice for Chinese (Taiwanese Mandarin, Traditional) (Free voice) */
+ public static final VoiceName HSIAO_YU = new VoiceName("HsiaoYu");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName HUMBERTO = new VoiceName("Humberto");
+
+ /** Male voice for Korean (Korea) (Free voice) */
+ public static final VoiceName HYUNSU = new VoiceName("Hyunsu");
+
+ /** Male voice for Korean (Korea) (Free voice) */
+ public static final VoiceName HYUNSU_2 = new VoiceName("Hyunsu_2");
+
+ /** Male voice for Korean (Korea) (Paid voice) */
+ public static final VoiceName HYUNSU_3 = new VoiceName("Hyunsu_3");
+
+ /** Female voice for Norwegian (Norway) (Free voice) */
+ public static final VoiceName IDA = new VoiceName("Ida");
+
+ /** Male voice for Albanian (Albania) (Free voice) */
+ public static final VoiceName ILIR = new VoiceName("Ilir");
+
+ /** Female voice for Arabic (Libya) (Free voice) */
+ public static final VoiceName IMAN = new VoiceName("Iman");
+
+ /** Female voice for English (Tanzania) (Free voice) */
+ public static final VoiceName IMANI = new VoiceName("Imani");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName IMELDA = new VoiceName("Imelda");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName INES = new VoiceName("Ines");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName INES_2 = new VoiceName("Ines_2");
+
+ /** Female voice for German (Austria) (Free voice) */
+ public static final VoiceName INGRID = new VoiceName("Ingrid");
+
+ /** Male voice for Korean (Korea) (Free voice) */
+ public static final VoiceName IN_JOON = new VoiceName("InJoon");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName IRENE = new VoiceName("Irene");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName IRMA = new VoiceName("Irma");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName ISABELLA = new VoiceName("Isabella");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName ISABELLA_2 = new VoiceName("Isabella_2");
+
+ /** Female voice for Italian (Italy) (Paid voice) */
+ public static final VoiceName ISABELLA_3 = new VoiceName("Isabella_3");
+
+ /** Female voice for French (Belgium) (Free voice) */
+ public static final VoiceName ISABELLE = new VoiceName("Isabelle");
+
+ /** Female voice for French (Belgium) (Paid voice) */
+ public static final VoiceName ISABELLE_2 = new VoiceName("Isabelle_2");
+
+ /** Female voice for Norwegian (Norway) (Free voice) */
+ public static final VoiceName ISELIN = new VoiceName("Iselin");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName ISIDORA = new VoiceName("Isidora");
+
+ /** Male voice for Arabic (Algeria) (Free voice) */
+ public static final VoiceName ISMAEL = new VoiceName("Ismael");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName IVY = new VoiceName("Ivy");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName IVY_2 = new VoiceName("Ivy_2");
+
+ /** Male voice for Polish (Poland) (Free voice) */
+ public static final VoiceName JACEK = new VoiceName("Jacek");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JACOB = new VoiceName("Jacob");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName JACQUELINE = new VoiceName("Jacqueline");
+
+ /** Male voice for Sundanese (Indonesia) (Free voice) */
+ public static final VoiceName JAJANG = new VoiceName("Jajang");
+
+ /** Male voice for Arabic (Morocco) (Free voice) */
+ public static final VoiceName JAMAL = new VoiceName("Jamal");
+
+ /** Male voice for English (Philippines) (Free voice) */
+ public static final VoiceName JAMES = new VoiceName("James");
+
+ /** Male voice for German (Switzerland) (Free voice) */
+ public static final VoiceName JAN = new VoiceName("Jan");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName JANE = new VoiceName("Jane");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName JANE_2 = new VoiceName("Jane_2");
+
+ /** Male voice for Polish (Poland) (Free voice) */
+ public static final VoiceName JANN = new VoiceName("Jann");
+
+ /** Female voice for English (Singapore) (Free voice) */
+ public static final VoiceName JASMINE = new VoiceName("Jasmine");
+
+ /** Female voice for English (Singapore) (Paid voice) */
+ public static final VoiceName JASMINE_2 = new VoiceName("Jasmine_2");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JASON = new VoiceName("Jason");
+
+ /** Male voice for Spanish (Equatorial Guinea) (Free voice) */
+ public static final VoiceName JAVIER = new VoiceName("Javier");
+
+ /** Male voice for French (Canada) (Free voice) */
+ public static final VoiceName JEAN = new VoiceName("Jean");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName JENNY = new VoiceName("Jenny");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName JENNY_2 = new VoiceName("Jenny_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName JENNY_3 = new VoiceName("Jenny_3");
+
+ /** Male voice for Danish (Denmark) (Free voice) */
+ public static final VoiceName JEPPE = new VoiceName("Jeppe");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName JEROME = new VoiceName("Jerome");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName JIHYE = new VoiceName("Jihye");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName JI_MIN = new VoiceName("JiMin");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JIMMIE = new VoiceName("Jimmie");
+
+ /** Female voice for Czech (Czechia) (Free voice) */
+ public static final VoiceName JITKA = new VoiceName("Jitka");
+
+ /** Female voice for Catalan (Free voice) */
+ public static final VoiceName JOANA = new VoiceName("Joana");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName JOANNA = new VoiceName("Joanna");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName JOANNA_2 = new VoiceName("Joanna_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName JOANNA_3 = new VoiceName("Joanna_3");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName JOANNE = new VoiceName("Joanne");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JOEY = new VoiceName("Joey");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JOEY_2 = new VoiceName("Joey_2");
+
+ /** Male voice for German (Austria) (Free voice) */
+ public static final VoiceName JONAS = new VoiceName("Jonas");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName JORGE = new VoiceName("Jorge");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName JORGE_2 = new VoiceName("Jorge_2");
+
+ /** Male voice for Maltese (Malta) (Free voice) */
+ public static final VoiceName JOSEPH = new VoiceName("Joseph");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName JOSEPHINE = new VoiceName("Josephine");
+
+ /** Male voice for Spanish (Costa Rica) (Free voice) */
+ public static final VoiceName JUAN = new VoiceName("Juan");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName JULIO = new VoiceName("Julio");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JUSTIN = new VoiceName("Justin");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName JUSTIN_2 = new VoiceName("Justin_2");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName KAI = new VoiceName("Kai");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName KAJAL = new VoiceName("Kajal");
+
+ /** Female voice for English (India) (Paid voice) */
+ public static final VoiceName KAJAL_2 = new VoiceName("Kajal_2");
+
+ /** Female voice for Bulgarian (Bulgaria) (Free voice) */
+ public static final VoiceName KALINA = new VoiceName("Kalina");
+
+ /** Female voice for Tamil (Malaysia) (Free voice) */
+ public static final VoiceName KANI = new VoiceName("Kani");
+
+ /** Female voice for Spanish (Puerto Rico) (Free voice) */
+ public static final VoiceName KARINA = new VoiceName("Karina");
+
+ /** Male voice for Icelandic (Iceland) (Free voice) */
+ public static final VoiceName KARL = new VoiceName("Karl");
+
+ /** Female voice for Spanish (Honduras) (Free voice) */
+ public static final VoiceName KARLA = new VoiceName("Karla");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName KASPER = new VoiceName("Kasper");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName KATJA = new VoiceName("Katja");
+
+ /** Female voice for Hindi (India) (Free voice) */
+ public static final VoiceName KAVYA = new VoiceName("Kavya");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName KAVYA_2 = new VoiceName("Kavya_2");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName KAZUHA = new VoiceName("Kazuha");
+
+ /** Male voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName KEITA = new VoiceName("Keita");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName KEN = new VoiceName("Ken");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName KENDRA = new VoiceName("Kendra");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName KENDRA_2 = new VoiceName("Kendra_2");
+
+ /** Female voice for Lao (Laos) (Free voice) */
+ public static final VoiceName KEOMANY = new VoiceName("Keomany");
+
+ /** Male voice for Estonian (Estonia) (Free voice) */
+ public static final VoiceName KERT = new VoiceName("Kert");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName KEVIN = new VoiceName("Kevin");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName KILLIAN = new VoiceName("Killian");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName KIM = new VoiceName("Kim");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName KIMBERLY = new VoiceName("Kimberly");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName KIMBERLY_2 = new VoiceName("Kimberly_2");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName KLARISSA = new VoiceName("Klarissa");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName KLAUS = new VoiceName("Klaus");
+
+ /** Male voice for Tamil (Sri Lanka) (Free voice) */
+ public static final VoiceName KUMAR = new VoiceName("Kumar");
+
+ /** Male voice for Hindi (India) (Free voice) */
+ public static final VoiceName KUNAL = new VoiceName("Kunal");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName KUNAL_2 = new VoiceName("Kunal_2");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName LAIA = new VoiceName("Laia");
+
+ /** Female voice for Arabic (Bahrain) (Free voice) */
+ public static final VoiceName LAILA = new VoiceName("Laila");
+
+ /** Male voice for Arabic (Syria) (Free voice) */
+ public static final VoiceName LAITH = new VoiceName("Laith");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName LARISSA = new VoiceName("Larissa");
+
+ /** Female voice for Pashto (Afghanistan) (Free voice) */
+ public static final VoiceName LATIFA = new VoiceName("Latifa");
+
+ /** Female voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName LAURA = new VoiceName("Laura");
+
+ /** Female voice for Dutch (Netherlands) (Paid voice) */
+ public static final VoiceName LAURA_2 = new VoiceName("Laura_2");
+
+ /** Female voice for Arabic (Lebanon) (Free voice) */
+ public static final VoiceName LAYLA = new VoiceName("Layla");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName LEA = new VoiceName("Lea");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName LEA_2 = new VoiceName("Lea_2");
+
+ /** Female voice for French (France) (Paid voice) */
+ public static final VoiceName LEA_3 = new VoiceName("Lea_3");
+
+ /** Female voice for English (South Africa) (Free voice) */
+ public static final VoiceName LEAH = new VoiceName("Leah");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName LEILA = new VoiceName("Leila");
+
+ /** Female voice for German (Switzerland) (Free voice) */
+ public static final VoiceName LENI = new VoiceName("Leni");
+
+ /** Male voice for German (Germany) (Paid voice) */
+ public static final VoiceName LENNART = new VoiceName("Lennart");
+
+ /** Male voice for Lithuanian (Lithuania) (Free voice) */
+ public static final VoiceName LEONAS = new VoiceName("Leonas");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName LETICIA = new VoiceName("Leticia");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName LEWIS = new VoiceName("Lewis");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName LIA = new VoiceName("Lia");
+
+ /** Male voice for English (Canada) (Free voice) */
+ public static final VoiceName LIAM = new VoiceName("Liam");
+
+ /** Male voice for French (Canada) (Free voice) */
+ public static final VoiceName LIAN = new VoiceName("Lian");
+
+ /** Male voice for French (Canada) (Paid voice) */
+ public static final VoiceName LIAN_2 = new VoiceName("Lian_2");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName LIBBY = new VoiceName("Libby");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName LIBERTO = new VoiceName("Liberto");
+
+ /** Female voice for Dutch (Belgium) (Free voice) */
+ public static final VoiceName LISA = new VoiceName("Lisa");
+
+ /** Female voice for Dutch (Belgium) (Paid voice) */
+ public static final VoiceName LISA_2 = new VoiceName("Lisa_2");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName LISANDRO = new VoiceName("Lisandro");
+
+ /** Female voice for Norwegian (Norway) (Free voice) */
+ public static final VoiceName LIV = new VoiceName("Liv");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName LIVIA = new VoiceName("Livia");
+
+ /** Female voice for English (Australia) (Paid voice) */
+ public static final VoiceName LIVIA_2 = new VoiceName("Livia_2");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName LOLA = new VoiceName("Lola");
+
+ /** Female voice for Spanish (El Salvador) (Free voice) */
+ public static final VoiceName LORENA = new VoiceName("Lorena");
+
+ /** Male voice for Italian (Italy) (Paid voice) */
+ public static final VoiceName LORENZIO = new VoiceName("Lorenzio");
+
+ /** Male voice for Spanish (Chile) (Free voice) */
+ public static final VoiceName LORENZO = new VoiceName("Lorenzo");
+
+ /** Female voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName LOTTE = new VoiceName("Lotte");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName LOUISA = new VoiceName("Louisa");
+
+ /** Female voice for Spanish (Castilian) (Free voice) */
+ public static final VoiceName LUCIA = new VoiceName("Lucia");
+
+ /** Female voice for Spanish (Castilian) (Free voice) */
+ public static final VoiceName LUCIA_2 = new VoiceName("Lucia_2");
+
+ /** Female voice for Spanish (Castilian) (Paid voice) */
+ public static final VoiceName LUCIA_3 = new VoiceName("Lucia_3");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName LUCIANO = new VoiceName("Luciano");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName LUCIEN = new VoiceName("Lucien");
+
+ /** Male voice for Spanish (Ecuador) (Free voice) */
+ public static final VoiceName LUIS = new VoiceName("Luis");
+
+ /** Male voice for Slovak (Slovakia) (Free voice) */
+ public static final VoiceName LUKAS = new VoiceName("Lukas");
+
+ /** Male voice for English (South Africa) (Free voice) */
+ public static final VoiceName LUKE = new VoiceName("Luke");
+
+ /** Female voice for English (Singapore) (Free voice) */
+ public static final VoiceName LUNA = new VoiceName("Luna");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName LUNA_2 = new VoiceName("Luna_2");
+
+ /** Female voice for Spanish (United States) (Free voice) */
+ public static final VoiceName LUPE = new VoiceName("Lupe");
+
+ /** Female voice for Spanish (United States) (Free voice) */
+ public static final VoiceName LUPE_2 = new VoiceName("Lupe_2");
+
+ /** Female voice for Spanish (United States) (Paid voice) */
+ public static final VoiceName LUPE_3 = new VoiceName("Lupe_3");
+
+ /** Male voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName MAARTEN = new VoiceName("Maarten");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName MACERIO = new VoiceName("Macerio");
+
+ /** Male voice for Portuguese (Brazil) (Paid voice) */
+ public static final VoiceName MACERIO_2 = new VoiceName("Macerio_2");
+
+ /** Male voice for Hindi (India) (Free voice) */
+ public static final VoiceName MADHUR = new VoiceName("Madhur");
+
+ /** Female voice for Uzbek (Latin, Uzbekistan) (Free voice) */
+ public static final VoiceName MADINA = new VoiceName("Madina");
+
+ /** Male voice for Danish (Denmark) (Free voice) */
+ public static final VoiceName MADS = new VoiceName("Mads");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName MAISIE = new VoiceName("Maisie");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName MAJA = new VoiceName("Maja");
+
+ /** Male voice for Marathi (India) (Free voice) */
+ public static final VoiceName MANOHAR = new VoiceName("Manohar");
+
+ /** Male voice for Spanish (Cuba) (Free voice) */
+ public static final VoiceName MANUEL = new VoiceName("Manuel");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName MANUELA = new VoiceName("Manuela");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName MARCELLO = new VoiceName("Marcello");
+
+ /** Male voice for Spanish (Bolivia) (Free voice) */
+ public static final VoiceName MARCELO = new VoiceName("Marcelo");
+
+ /** Male voice for Polish (Poland) (Free voice) */
+ public static final VoiceName MAREK = new VoiceName("Marek");
+
+ /** Female voice for Spanish (Panama) (Free voice) */
+ public static final VoiceName MARGARITA = new VoiceName("Margarita");
+
+ /** Female voice for Spanish (Costa Rica) (Free voice) */
+ public static final VoiceName MARIA = new VoiceName("Maria");
+
+ /** Female voice for Macedonian (North Macedonia) (Free voice) */
+ public static final VoiceName MARIJA = new VoiceName("Marija");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName MARINA = new VoiceName("Marina");
+
+ /** Male voice for Spanish (Paraguay) (Free voice) */
+ public static final VoiceName MARIO = new VoiceName("Mario");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName MARLENE = new VoiceName("Marlene");
+
+ /** Female voice for Spanish (Guatemala) (Free voice) */
+ public static final VoiceName MARTA = new VoiceName("Marta");
+
+ /** Female voice for Arabic (Yemen) (Free voice) */
+ public static final VoiceName MARYAM = new VoiceName("Maryam");
+
+ /** Male voice for Japanese (Japan) (Paid voice) */
+ public static final VoiceName MASARU = new VoiceName("Masaru");
+
+ /** Male voice for Spanish (Uruguay) (Free voice) */
+ public static final VoiceName MATEO = new VoiceName("Mateo");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName MATHIEU = new VoiceName("Mathieu");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName MATTHEW = new VoiceName("Matthew");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName MATTHEW_2 = new VoiceName("Matthew_2");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName MATTHEW_3 = new VoiceName("Matthew_3");
+
+ /** Male voice for Swedish (Sweden) (Free voice) */
+ public static final VoiceName MATTIAS = new VoiceName("Mattias");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName MAURICE = new VoiceName("Maurice");
+
+ /** Male voice for Russian (Russia) (Free voice) */
+ public static final VoiceName MAXIM = new VoiceName("Maxim");
+
+ /** Female voice for Polish (Poland) (Free voice) */
+ public static final VoiceName MAYA = new VoiceName("Maya");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName MAYU = new VoiceName("Mayu");
+
+ /** Female voice for Amharic (Ethiopia) (Free voice) */
+ public static final VoiceName MEKDES = new VoiceName("Mekdes");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName MIA = new VoiceName("Mia");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName MIA_2 = new VoiceName("Mia_2");
+
+ /** Female voice for Spanish (Mexico) (Paid voice) */
+ public static final VoiceName MIA_3 = new VoiceName("Mia_3");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName MICHELLE = new VoiceName("Michelle");
+
+ /** Male voice for Malayalam (India) (Free voice) */
+ public static final VoiceName MIDHUN = new VoiceName("Midhun");
+
+ /** Male voice for Spanish (United States) (Free voice) */
+ public static final VoiceName MIGUEL = new VoiceName("Miguel");
+
+ /** Male voice for English (New Zealand) (Free voice) */
+ public static final VoiceName MITCHELL = new VoiceName("Mitchell");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName MIZUKI = new VoiceName("Mizuki");
+
+ /** Male voice for Arabic (Qatar) (Free voice) */
+ public static final VoiceName MOAZ = new VoiceName("Moaz");
+
+ /** Male voice for Telugu (India) (Free voice) */
+ public static final VoiceName MOHAN = new VoiceName("Mohan");
+
+ /** Female voice for English (New Zealand) (Free voice) */
+ public static final VoiceName MOLLY = new VoiceName("Molly");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName MONICA = new VoiceName("Monica");
+
+ /** Female voice for Arabic (Morocco) (Free voice) */
+ public static final VoiceName MOUNA = new VoiceName("Mouna");
+
+ /** Male voice for Somali (Somalia) (Free voice) */
+ public static final VoiceName MUUSE = new VoiceName("Muuse");
+
+ /** Female voice for Bangla (Bangladesh) (Free voice) */
+ public static final VoiceName NABANITA = new VoiceName("Nabanita");
+
+ /** Female voice for Danish (Denmark) (Free voice) */
+ public static final VoiceName NAJA = new VoiceName("Naja");
+
+ /** Male voice for Vietnamese (Vietnam) (Free voice) */
+ public static final VoiceName NAM_MINH = new VoiceName("NamMinh");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName NANAMI = new VoiceName("Nanami");
+
+ /** Female voice for Japanese (Japan) (Paid voice) */
+ public static final VoiceName NANAMI_2 = new VoiceName("Nanami_2");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName NANCY = new VoiceName("Nancy");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName NANCY_2 = new VoiceName("Nancy_2");
+
+ /** Male voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName NAOKI = new VoiceName("Naoki");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName NATASHA = new VoiceName("Natasha");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName NEERJA = new VoiceName("Neerja");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName NEERJA_2 = new VoiceName("Neerja_2");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName NEIL = new VoiceName("Neil");
+
+ /** Male voice for Greek (Greece) (Free voice) */
+ public static final VoiceName NESTORAS = new VoiceName("Nestoras");
+
+ /** Female voice for Welsh (United Kingdom) (Free voice) */
+ public static final VoiceName NIA = new VoiceName("Nia");
+
+ /** Female voice for English (Ireland) (Free voice) */
+ public static final VoiceName NIAMH = new VoiceName("Niamh");
+
+ /** Female voice for English (Ireland) (Paid voice) */
+ public static final VoiceName NIAMH_2 = new VoiceName("Niamh_2");
+
+ /** Male voice for Serbian (Cyrillic, Serbia) (Free voice) */
+ public static final VoiceName NICHOLAS = new VoiceName("Nicholas");
+
+ /** Male voice for Serbian (Latin, Serbia) (Free voice) */
+ public static final VoiceName NICHOLAS_2 = new VoiceName("Nicholas_2");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName NICOLAU = new VoiceName("Nicolau");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName NICOLE = new VoiceName("Nicole");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName NIL = new VoiceName("Nil");
+
+ /** Female voice for Burmese (Myanmar) (Free voice) */
+ public static final VoiceName NILAR = new VoiceName("Nilar");
+
+ /** Male voice for Latvian (Latvia) (Free voice) */
+ public static final VoiceName NILS = new VoiceName("Nils");
+
+ /** Male voice for Gujarati (India) (Free voice) */
+ public static final VoiceName NIRANJAN = new VoiceName("Niranjan");
+
+ /** Male voice for Thai (Thailand) (Free voice) */
+ public static final VoiceName NIWAT = new VoiceName("Niwat");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName NOAH = new VoiceName("Noah");
+
+ /** Female voice for Hungarian (Hungary) (Free voice) */
+ public static final VoiceName NOEMI = new VoiceName("Noemi");
+
+ /** Female voice for Finnish (Finland) (Free voice) */
+ public static final VoiceName NOORA = new VoiceName("Noora");
+
+ /** Female voice for Arabic (Kuwait) (Free voice) */
+ public static final VoiceName NOURA = new VoiceName("Noura");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName NOVA = new VoiceName("Nova");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName NOVA_2 = new VoiceName("Nova_2");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName NURIA = new VoiceName("Nuria");
+
+ /** Male voice for Punjabi (India) (Free voice) */
+ public static final VoiceName OJAS = new VoiceName("Ojas");
+
+ /** Female voice for Polish (Poland) (Free voice) */
+ public static final VoiceName OLA = new VoiceName("Ola");
+
+ /** Female voice for Polish (Poland) (Paid voice) */
+ public static final VoiceName OLA_2 = new VoiceName("Ola_2");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName OLIVER = new VoiceName("Oliver");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName OLIVIA = new VoiceName("Olivia");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName OLLIE = new VoiceName("Ollie");
+
+ /** Male voice for English (United Kingdom) (Paid voice) */
+ public static final VoiceName OLLIE_2 = new VoiceName("Ollie_2");
+
+ /** Male voice for Arabic (Libya) (Free voice) */
+ public static final VoiceName OMAR = new VoiceName("Omar");
+
+ /** Female voice for Lithuanian (Lithuania) (Free voice) */
+ public static final VoiceName ONA = new VoiceName("Ona");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ONYX = new VoiceName("Onyx");
+
+ /** Female voice for Irish (Ireland) (Free voice) */
+ public static final VoiceName ORLA = new VoiceName("Orla");
+
+ /** Male voice for Malay (Malaysia) (Free voice) */
+ public static final VoiceName OSMAN = new VoiceName("Osman");
+
+ /** Male voice for Ukrainian (Ukraine) (Free voice) */
+ public static final VoiceName OSTAP = new VoiceName("Ostap");
+
+ /** Female voice for Tamil (India) (Free voice) */
+ public static final VoiceName PALLAVI = new VoiceName("Pallavi");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName PALMIRA = new VoiceName("Palmira");
+
+ /** Female voice for Spanish (United States) (Free voice) */
+ public static final VoiceName PALOMA = new VoiceName("Paloma");
+
+ /** Female voice for Spanish (Venezuela) (Free voice) */
+ public static final VoiceName PAOLA = new VoiceName("Paola");
+
+ /** Male voice for Spanish (United States) (Free voice) */
+ public static final VoiceName PEDRO = new VoiceName("Pedro");
+
+ /** Male voice for Spanish (United States) (Paid voice) */
+ public static final VoiceName PEDRO_2 = new VoiceName("Pedro_2");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName PELAYO = new VoiceName("Pelayo");
+
+ /** Female voice for Spanish (United States) (Free voice) */
+ public static final VoiceName PENELOPE = new VoiceName("Penelope");
+
+ /** Female voice for Norwegian (Norway) (Free voice) */
+ public static final VoiceName PERNILLE = new VoiceName("Pernille");
+
+ /** Female voice for Slovenian (Slovenia) (Free voice) */
+ public static final VoiceName PETRA = new VoiceName("Petra");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName PHOEBE = new VoiceName("Phoebe");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName PHOEBE_2 = new VoiceName("Phoebe_2");
+
+ /** Female voice for Italian (Italy) (Free voice) */
+ public static final VoiceName PIERINA = new VoiceName("Pierina");
+
+ /** Male voice for Khmer (Cambodia) (Free voice) */
+ public static final VoiceName PISETH = new VoiceName("Piseth");
+
+ /** Female voice for Ukrainian (Ukraine) (Free voice) */
+ public static final VoiceName POLINA = new VoiceName("Polina");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName PRABHAT = new VoiceName("Prabhat");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName PRABHAT_2 = new VoiceName("Prabhat_2");
+
+ /** Male voice for Bangla (Bangladesh) (Free voice) */
+ public static final VoiceName PRADEEP = new VoiceName("Pradeep");
+
+ /** Female voice for Thai (Thailand) (Free voice) */
+ public static final VoiceName PREMWADEE = new VoiceName("Premwadee");
+
+ /** Male voice for Assamese (India) (Free voice) */
+ public static final VoiceName PRIYOM = new VoiceName("Priyom");
+
+ /** Male voice for Swahili (Kenya) (Free voice) */
+ public static final VoiceName RAFIKI = new VoiceName("Rafiki");
+
+ /** Male voice for German (Germany) (Free voice) */
+ public static final VoiceName RALF = new VoiceName("Ralf");
+
+ /** Male voice for Arabic (Lebanon) (Free voice) */
+ public static final VoiceName RAMI = new VoiceName("Rami");
+
+ /** Female voice for Spanish (Dominican Republic) (Free voice) */
+ public static final VoiceName RAMONA = new VoiceName("Ramona");
+
+ /** Female voice for Arabic (Iraq) (Free voice) */
+ public static final VoiceName RANA = new VoiceName("Rana");
+
+ /** Female voice for Portuguese (Portugal) (Free voice) */
+ public static final VoiceName RAQUEL = new VoiceName("Raquel");
+
+ /** Female voice for English (India) (Free voice) */
+ public static final VoiceName RAVEENA = new VoiceName("Raveena");
+
+ /** Female voice for Arabic (Tunisia) (Free voice) */
+ public static final VoiceName REEM = new VoiceName("Reem");
+
+ /** Male voice for Hindi (India) (Free voice) */
+ public static final VoiceName REHAAN = new VoiceName("Rehaan");
+
+ /** Male voice for English (India) (Free voice) */
+ public static final VoiceName REHAAN_2 = new VoiceName("Rehaan_2");
+
+ /** Female voice for Swahili (Tanzania) (Free voice) */
+ public static final VoiceName REHEMA = new VoiceName("Rehema");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName REMI = new VoiceName("Remi");
+
+ /** Male voice for French (France) (Paid voice) */
+ public static final VoiceName REMI_2 = new VoiceName("Remi_2");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName REMY = new VoiceName("Remy");
+
+ /** Male voice for French (France) (Paid voice) */
+ public static final VoiceName REMY_2 = new VoiceName("Remy_2");
+
+ /** Female voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName RENATA = new VoiceName("Renata");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName RICARDO = new VoiceName("Ricardo");
+
+ /** Male voice for Italian (Italy) (Free voice) */
+ public static final VoiceName RINALDO = new VoiceName("Rinaldo");
+
+ /** Male voice for Spanish (Panama) (Free voice) */
+ public static final VoiceName ROBERTO = new VoiceName("Roberto");
+
+ /** Male voice for Spanish (El Salvador) (Free voice) */
+ public static final VoiceName RODRIGO = new VoiceName("Rodrigo");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName ROGER = new VoiceName("Roger");
+
+ /** Male voice for Galician (Free voice) */
+ public static final VoiceName ROI = new VoiceName("Roi");
+
+ /** Male voice for Slovenian (Slovenia) (Free voice) */
+ public static final VoiceName ROK = new VoiceName("Rok");
+
+ /** Female voice for English (Philippines) (Free voice) */
+ public static final VoiceName ROSA = new VoiceName("Rosa");
+
+ /** Male voice for Dutch (Netherlands) (Free voice) */
+ public static final VoiceName RUBEN = new VoiceName("Ruben");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName RUSSELL = new VoiceName("Russell");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName RUTH = new VoiceName("Ruth");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName RUTH_2 = new VoiceName("Ruth_2");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName RYAN = new VoiceName("Ryan");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName RYAN_2 = new VoiceName("Ryan_2");
+
+ /** Female voice for Galician (Free voice) */
+ public static final VoiceName SABELA = new VoiceName("Sabela");
+
+ /** Female voice for German (Switzerland) (Free voice) */
+ public static final VoiceName SABRINA = new VoiceName("Sabrina");
+
+ /** Female voice for German (Switzerland) (Paid voice) */
+ public static final VoiceName SABRINA_2 = new VoiceName("Sabrina_2");
+
+ /** Male voice for Nepali (Nepal) (Free voice) */
+ public static final VoiceName SAGAR = new VoiceName("Sagar");
+
+ /** Male voice for Arabic (Yemen) (Free voice) */
+ public static final VoiceName SALEH = new VoiceName("Saleh");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName SALLI = new VoiceName("Salli");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName SALLI_2 = new VoiceName("Salli_2");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName SALLI_3 = new VoiceName("Salli_3");
+
+ /** Female voice for Arabic (Egypt) (Free voice) */
+ public static final VoiceName SALMA = new VoiceName("Salma");
+
+ /** Male voice for Urdu (India) (Free voice) */
+ public static final VoiceName SALMAN = new VoiceName("Salman");
+
+ /** Female voice for Spanish (Colombia) (Free voice) */
+ public static final VoiceName SALOME = new VoiceName("Salome");
+
+ /** Male voice for English (Hong Kong SAR) (Free voice) */
+ public static final VoiceName SAM = new VoiceName("Sam");
+
+ /** Male voice for Sinhala (Sri Lanka) (Free voice) */
+ public static final VoiceName SAMEERA = new VoiceName("Sameera");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName SAMUEL = new VoiceName("Samuel");
+
+ /** Female voice for Arabic (Jordan) (Free voice) */
+ public static final VoiceName SANA = new VoiceName("Sana");
+
+ /** Female voice for Kannada (India) (Free voice) */
+ public static final VoiceName SAPNA = new VoiceName("Sapna");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName SARA = new VoiceName("Sara");
+
+ /** Female voice for Tamil (Sri Lanka) (Free voice) */
+ public static final VoiceName SARANYA = new VoiceName("Saranya");
+
+ /** Male voice for Uzbek (Latin, Uzbekistan) (Free voice) */
+ public static final VoiceName SARDOR = new VoiceName("Sardor");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName SAUL = new VoiceName("Saul");
+
+ /** Male voice for Spanish (Venezuela) (Free voice) */
+ public static final VoiceName SEBASTIAN = new VoiceName("Sebastian");
+
+ /** Female voice for Finnish (Finland) (Free voice) */
+ public static final VoiceName SELMA = new VoiceName("Selma");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName SEO_HYEON = new VoiceName("SeoHyeon");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName SEOYEON = new VoiceName("Seoyeon");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName SEOYEON_2 = new VoiceName("Seoyeon_2");
+
+ /** Female voice for Korean (Korea) (Paid voice) */
+ public static final VoiceName SEOYEON_3 = new VoiceName("Seoyeon_3");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName SERAPHINA = new VoiceName("Seraphina");
+
+ /** Female voice for German (Germany) (Paid voice) */
+ public static final VoiceName SERAPHINA_2 = new VoiceName("Seraphina_2");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName SERENA = new VoiceName("Serena");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName SERENA_2 = new VoiceName("Serena_2");
+
+ /** Male voice for Spanish (Castilian) (Free voice) */
+ public static final VoiceName SERGIO = new VoiceName("Sergio");
+
+ /** Male voice for Spanish (Castilian) (Paid voice) */
+ public static final VoiceName SERGIO_2 = new VoiceName("Sergio_2");
+
+ /** Male voice for Arabic (Egypt) (Free voice) */
+ public static final VoiceName SHAKIR = new VoiceName("Shakir");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName SHIMMER = new VoiceName("Shimmer");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName SHIORI = new VoiceName("Shiori");
+
+ /** Female voice for Telugu (India) (Free voice) */
+ public static final VoiceName SHRUTI = new VoiceName("Shruti");
+
+ /** Female voice for Inuktitut (Syllabics, Canada) (Free voice) */
+ public static final VoiceName SIQINIQ = new VoiceName("Siqiniq");
+
+ /** Female voice for Inuktitut (Latin, Canada) (Free voice) */
+ public static final VoiceName SIQINIQ_2 = new VoiceName("Siqiniq_2");
+
+ /** Female voice for Javanese (Latin, Indonesia) (Free voice) */
+ public static final VoiceName SITI = new VoiceName("Siti");
+
+ /** Female voice for Malayalam (India) (Free voice) */
+ public static final VoiceName SOBHANA = new VoiceName("Sobhana");
+
+ /** Female voice for Danish (Denmark) (Free voice) */
+ public static final VoiceName SOFFIE = new VoiceName("Soffie");
+
+ /** Female voice for Spanish (Bolivia) (Free voice) */
+ public static final VoiceName SOFIA = new VoiceName("Sofia");
+
+ /** Female voice for Swedish (Sweden) (Free voice) */
+ public static final VoiceName SOFIE = new VoiceName("Sofie");
+
+ /** Female voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName SONIA = new VoiceName("Sonia");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName SOON_BOK = new VoiceName("SoonBok");
+
+ /** Female voice for Serbian (Cyrillic, Serbia) (Free voice) */
+ public static final VoiceName SOPHIE = new VoiceName("Sophie");
+
+ /** Female voice for Serbian (Latin, Serbia) (Free voice) */
+ public static final VoiceName SOPHIE_2 = new VoiceName("Sophie_2");
+
+ /** Male voice for Croatian (Croatia) (Free voice) */
+ public static final VoiceName SRECKO = new VoiceName("Srecko");
+
+ /** Female voice for Khmer (Cambodia) (Free voice) */
+ public static final VoiceName SREYMOM = new VoiceName("Sreymom");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName STEFFAN = new VoiceName("Steffan");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName STEFFAN_2 = new VoiceName("Steffan_2");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName STEFFAN_3 = new VoiceName("Steffan_3");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName STEPHEN = new VoiceName("Stephen");
+
+ /** Male voice for English (United States) (Paid voice) */
+ public static final VoiceName STEPHEN_2 = new VoiceName("Stephen_2");
+
+ /** Female voice for Odia (India) (Free voice) */
+ public static final VoiceName SUBHASINI = new VoiceName("Subhasini");
+
+ /** Male voice for Odia (India) (Free voice) */
+ public static final VoiceName SUKANT = new VoiceName("Sukant");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName SUN_HI = new VoiceName("SunHi");
+
+ /** Female voice for Korean (Korea) (Paid voice) */
+ public static final VoiceName SUN_HI_2 = new VoiceName("SunHi_2");
+
+ /** Male voice for Tamil (Malaysia) (Free voice) */
+ public static final VoiceName SURYA = new VoiceName("Surya");
+
+ /** Female voice for Finnish (Finland) (Free voice) */
+ public static final VoiceName SUVI = new VoiceName("Suvi");
+
+ /** Female voice for Russian (Russia) (Free voice) */
+ public static final VoiceName SVETLANA = new VoiceName("Svetlana");
+
+ /** Female voice for Hindi (India) (Free voice) */
+ public static final VoiceName SWARA = new VoiceName("Swara");
+
+ /** Female voice for French (Canada) (Free voice) */
+ public static final VoiceName SYLVIE = new VoiceName("Sylvie");
+
+ /** Female voice for French (Canada) (Paid voice) */
+ public static final VoiceName SYLVIE_2 = new VoiceName("Sylvie_2");
+
+ /** Male voice for Arabic (Jordan) (Free voice) */
+ public static final VoiceName TAIM = new VoiceName("Taim");
+
+ /** Male voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName TAKUMI = new VoiceName("Takumi");
+
+ /** Male voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName TAKUMI_2 = new VoiceName("Takumi_2");
+
+ /** Male voice for Hungarian (Hungary) (Free voice) */
+ public static final VoiceName TAMAS = new VoiceName("Tamas");
+
+ /** Female voice for Spanish (Paraguay) (Free voice) */
+ public static final VoiceName TANIA = new VoiceName("Tania");
+
+ /** Female voice for Bengali (India) (Free voice) */
+ public static final VoiceName TANISHAA = new VoiceName("Tanishaa");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName TANJA = new VoiceName("Tanja");
+
+ /** Male voice for Inuktitut (Syllabics, Canada) (Free voice) */
+ public static final VoiceName TAQQIQ = new VoiceName("Taqqiq");
+
+ /** Male voice for Inuktitut (Latin, Canada) (Free voice) */
+ public static final VoiceName TAQQIQ_2 = new VoiceName("Taqqiq_2");
+
+ /** Female voice for Russian (Russia) (Free voice) */
+ public static final VoiceName TATYANA = new VoiceName("Tatyana");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName TEO = new VoiceName("Teo");
+
+ /** Female voice for Spanish (Equatorial Guinea) (Free voice) */
+ public static final VoiceName TERESA = new VoiceName("Teresa");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName THALITA = new VoiceName("Thalita");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName THALITA_2 = new VoiceName("Thalita_2");
+
+ /** Female voice for Portuguese (Brazil) (Paid voice) */
+ public static final VoiceName THALITA_3 = new VoiceName("Thalita_3");
+
+ /** Female voice for Zulu (South Africa) (Free voice) */
+ public static final VoiceName THANDO = new VoiceName("Thando");
+
+ /** Male voice for Zulu (South Africa) (Free voice) */
+ public static final VoiceName THEMBA = new VoiceName("Themba");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName THIAGO = new VoiceName("Thiago");
+
+ /** Male voice for French (Canada) (Free voice) */
+ public static final VoiceName THIERRY = new VoiceName("Thierry");
+
+ /** Male voice for French (Canada) (Paid voice) */
+ public static final VoiceName THIERRY_2 = new VoiceName("Thierry_2");
+
+ /** Male voice for Burmese (Myanmar) (Free voice) */
+ public static final VoiceName THIHA = new VoiceName("Thiha");
+
+ /** Female voice for Sinhala (Sri Lanka) (Free voice) */
+ public static final VoiceName THILINI = new VoiceName("Thilini");
+
+ /** Male voice for English (United Kingdom) (Free voice) */
+ public static final VoiceName THOMAS = new VoiceName("Thomas");
+
+ /** Female voice for English (United States) (Free voice) */
+ public static final VoiceName TIANA = new VoiceName("Tiana");
+
+ /** Female voice for English (United States) (Paid voice) */
+ public static final VoiceName TIFFANY = new VoiceName("Tiffany");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName TIM = new VoiceName("Tim");
+
+ /** Female voice for English (Australia) (Free voice) */
+ public static final VoiceName TINA = new VoiceName("Tina");
+
+ /** Male voice for Spanish (Argentina) (Free voice) */
+ public static final VoiceName TOMAS = new VoiceName("Tomas");
+
+ /** Female voice for Japanese (Japan) (Free voice) */
+ public static final VoiceName TOMOKO = new VoiceName("Tomoko");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName TONY = new VoiceName("Tony");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName TRIANA = new VoiceName("Triana");
+
+ /** Male voice for Spanish (Mexico) (Paid voice) */
+ public static final VoiceName TRISTAN = new VoiceName("Tristan");
+
+ /** Male voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName TRISTAN_ES = new VoiceName("Tristan_ES");
+
+ /** Male voice for Spanish (Spain) (Paid voice) */
+ public static final VoiceName TRISTAN_ES_2 = new VoiceName("Tristan_ES_2");
+
+ /** Female voice for Sundanese (Indonesia) (Free voice) */
+ public static final VoiceName TUTI = new VoiceName("Tuti");
+
+ /** Male voice for English (United States) (Free voice) */
+ public static final VoiceName TYLER = new VoiceName("Tyler");
+
+ /** Female voice for Somali (Somalia) (Free voice) */
+ public static final VoiceName UBAX = new VoiceName("Ubax");
+
+ /** Female voice for Urdu (Pakistan) (Free voice) */
+ public static final VoiceName UZMA = new VoiceName("Uzma");
+
+ /** Female voice for Punjabi (India) (Free voice) */
+ public static final VoiceName VAANI = new VoiceName("Vaani");
+
+ /** Female voice for Spanish (Uruguay) (Free voice) */
+ public static final VoiceName VALENTINA = new VoiceName("Valentina");
+
+ /** Male voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName VALERIO = new VoiceName("Valerio");
+
+ /** Male voice for Tamil (India) (Free voice) */
+ public static final VoiceName VALLUVAR = new VoiceName("Valluvar");
+
+ /** Female voice for Tamil (Singapore) (Free voice) */
+ public static final VoiceName VENBA = new VoiceName("Venba");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName VERA = new VoiceName("Vera");
+
+ /** Female voice for Bosnian (Bosnia and Herzegovina) (Free voice) */
+ public static final VoiceName VESNA = new VoiceName("Vesna");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName VICKI = new VoiceName("Vicki");
+
+ /** Female voice for German (Germany) (Free voice) */
+ public static final VoiceName VICKI_2 = new VoiceName("Vicki_2");
+
+ /** Female voice for German (Germany) (Paid voice) */
+ public static final VoiceName VICKI_3 = new VoiceName("Vicki_3");
+
+ /** Male voice for Spanish (Puerto Rico) (Free voice) */
+ public static final VoiceName VICTOR = new VoiceName("Victor");
+
+ /** Female voice for Slovak (Slovakia) (Free voice) */
+ public static final VoiceName VIKTORIA = new VoiceName("Viktoria");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName VITORIA = new VoiceName("Vitoria");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName VITORIA_2 = new VoiceName("Vitoria_2");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName VIVIENNE = new VoiceName("Vivienne");
+
+ /** Female voice for French (France) (Paid voice) */
+ public static final VoiceName VIVIENNE_2 = new VoiceName("Vivienne_2");
+
+ /** Female voice for Czech (Czechia) (Free voice) */
+ public static final VoiceName VLASTA = new VoiceName("Vlasta");
+
+ /** Male voice for Chinese (Cantonese, Traditional) (Free voice) */
+ public static final VoiceName WAN_LUNG = new VoiceName("WanLung");
+
+ /** Male voice for English (Singapore) (Free voice) */
+ public static final VoiceName WAYNE = new VoiceName("Wayne");
+
+ /** Male voice for Afrikaans (South Africa) (Free voice) */
+ public static final VoiceName WILLEM = new VoiceName("Willem");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName WILLIAM = new VoiceName("William");
+
+ /** Male voice for English (Australia) (Free voice) */
+ public static final VoiceName WILLIAM_2 = new VoiceName("William_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOCHEN = new VoiceName("Xiaochen");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOCHEN_2 = new VoiceName("Xiaochen_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOCHEN_3 = new VoiceName("Xiaochen_3");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Paid voice) */
+ public static final VoiceName XIAOCHEN_4 = new VoiceName("Xiaochen_4");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOHAN = new VoiceName("Xiaohan");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOHAN_2 = new VoiceName("Xiaohan_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOMENG = new VoiceName("Xiaomeng");
+
+ /** Female voice for Chinese (Cantonese, Simplified) (Free voice) */
+ public static final VoiceName XIAO_MIN = new VoiceName("XiaoMin");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOMO = new VoiceName("Xiaomo");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOQIU = new VoiceName("Xiaoqiu");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOROU = new VoiceName("Xiaorou");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAORUI = new VoiceName("Xiaorui");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOSHUANG = new VoiceName("Xiaoshuang");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOSHUANG_2 = new VoiceName("Xiaoshuang_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOSHUANG_3 = new VoiceName("Xiaoshuang_3");
+
+ /** Female voice for Chinese (Wu, Simplified) (Free voice) */
+ public static final VoiceName XIAOTONG = new VoiceName("Xiaotong");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOXIAO = new VoiceName("Xiaoxiao");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOXIAO_2 = new VoiceName("Xiaoxiao_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOXIAO_3 = new VoiceName("Xiaoxiao_3");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOXIAO_4 = new VoiceName("Xiaoxiao_4");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOXIAO_5 = new VoiceName("Xiaoxiao_5");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYAN = new VoiceName("Xiaoyan");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYI = new VoiceName("Xiaoyi");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYI_2 = new VoiceName("Xiaoyi_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYOU = new VoiceName("Xiaoyou");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYOU_2 = new VoiceName("Xiaoyou_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYOU_3 = new VoiceName("Xiaoyou_3");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYU = new VoiceName("Xiaoyu");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOYU_2 = new VoiceName("Xiaoyu_2");
+
+ /** Female voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName XIAOZHEN = new VoiceName("Xiaozhen");
+
+ /** Female voice for Spanish (Mexico) (Paid voice) */
+ public static final VoiceName XIMENA = new VoiceName("Ximena");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName XIMENA_ES = new VoiceName("Ximena_ES");
+
+ /** Female voice for Spanish (Spain) (Free voice) */
+ public static final VoiceName XIMENA_ES_2 = new VoiceName("Ximena_ES_2");
+
+ /** Female voice for Spanish (Spain) (Paid voice) */
+ public static final VoiceName XIMENA_ES_3 = new VoiceName("Ximena_ES_3");
+
+ /** Male voice for Spanish (Mexico) (Free voice) */
+ public static final VoiceName YAGO = new VoiceName("Yago");
+
+ /** Female voice for English (Hong Kong SAR) (Free voice) */
+ public static final VoiceName YAN = new VoiceName("Yan");
+
+ /** Female voice for Portuguese (Brazil) (Free voice) */
+ public static final VoiceName YARA = new VoiceName("Yara");
+
+ /** Female voice for Assamese (India) (Free voice) */
+ public static final VoiceName YASHICA = new VoiceName("Yashica");
+
+ /** Female voice for Malay (Malaysia) (Free voice) */
+ public static final VoiceName YASMIN = new VoiceName("Yasmin");
+
+ /** Female voice for Mongolian (Mongolia) (Free voice) */
+ public static final VoiceName YESUI = new VoiceName("Yesui");
+
+ /** Female voice for Spanish (Nicaragua) (Free voice) */
+ public static final VoiceName YOLANDA = new VoiceName("Yolanda");
+
+ /** Female voice for Korean (Korea) (Free voice) */
+ public static final VoiceName YU_JIN = new VoiceName("YuJin");
+
+ /** Male voice for Chinese (Zhongyuan Mandarin Henan, Simplified) (Free voice) */
+ public static final VoiceName YUNDENG = new VoiceName("Yundeng");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNFAN = new VoiceName("Yunfan");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Paid voice) */
+ public static final VoiceName YUNFAN_2 = new VoiceName("Yunfan_2");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNFENG = new VoiceName("Yunfeng");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNHAN = new VoiceName("Yunhan");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNHAO = new VoiceName("Yunhao");
+
+ /** Male voice for Chinese (Taiwanese Mandarin, Traditional) (Free voice) */
+ public static final VoiceName YUN_JHE = new VoiceName("YunJhe");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNJIAN = new VoiceName("Yunjian");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNJIE = new VoiceName("Yunjie");
+
+ /** Male voice for Chinese (Cantonese, Simplified) (Free voice) */
+ public static final VoiceName YUN_SONG = new VoiceName("YunSong");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXI = new VoiceName("Yunxi");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXI_2 = new VoiceName("Yunxi_2");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXIA = new VoiceName("Yunxia");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXIA_2 = new VoiceName("Yunxia_2");
+
+ /** Male voice for Chinese (Jilu Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXIANG = new VoiceName("Yunxiang");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXIAO = new VoiceName("Yunxiao");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNXIAO_2 = new VoiceName("Yunxiao_2");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNYANG = new VoiceName("Yunyang");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNYE = new VoiceName("Yunye");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNYE_2 = new VoiceName("Yunye_2");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNYI = new VoiceName("Yunyi");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNYI_2 = new VoiceName("Yunyi_2");
+
+ /** Male voice for Chinese (Mandarin, Simplified) (Free voice) */
+ public static final VoiceName YUNZE = new VoiceName("Yunze");
+
+ /** Male voice for Chinese (Wu, Simplified) (Free voice) */
+ public static final VoiceName YUNZHE = new VoiceName("Yunzhe");
+
+ /** Male voice for French (France) (Free voice) */
+ public static final VoiceName YVES = new VoiceName("Yves");
+
+ /** Female voice for French (France) (Free voice) */
+ public static final VoiceName YVETTE = new VoiceName("Yvette");
+
+ /** Female voice for Arabic (Saudi Arabia) (Free voice) */
+ public static final VoiceName ZARIYAH = new VoiceName("Zariyah");
+
+ /** Male voice for Arabic (Saudi Arabia) (Free voice) */
+ public static final VoiceName ZAYD = new VoiceName("Zayd");
+
+ /** Female voice for Arabic (Saudi Arabia) (Free voice) */
+ public static final VoiceName ZEINA = new VoiceName("Zeina");
+
+ /** Female voice for Chinese (Mandarin) (Free voice) */
+ public static final VoiceName ZHIYU = new VoiceName("Zhiyu");
+
+ /** Female voice for Chinese (Mandarin) (Free voice) */
+ public static final VoiceName ZHIYU_2 = new VoiceName("Zhiyu_2");
+
+ /** Female voice for Polish (Poland) (Free voice) */
+ public static final VoiceName ZOFIA = new VoiceName("Zofia");
+
+ /** Female voice for Swahili (Kenya) (Free voice) */
+ public static final VoiceName ZURI = new VoiceName("Zuri");
+
+ private static final EnumSupportDynamic ENUM_SUPPORT =
+ new EnumSupportDynamic<>(
+ VoiceName.class,
+ VoiceName::new,
+ Arrays.asList(
+ AARAV,
+ AARAV_2,
+ AAROHI,
+ AARTI,
+ AARTI_2,
+ AARTI_3,
+ AASHI,
+ ABBI,
+ ABDULLAH,
+ ABEO,
+ ABRIL,
+ ACHARA,
+ ADA,
+ ADA_2,
+ ADAM,
+ ADAM_2,
+ ADITI,
+ ADRI,
+ ADRIANO,
+ AGNIESZKA,
+ AHMET,
+ AIGUL,
+ AINHOA,
+ ALAIN,
+ ALBA,
+ ALED,
+ ALEKSANDAR,
+ ALESSIO,
+ ALESSIO_2,
+ ALEX,
+ ALFIE,
+ ALI,
+ ALINA,
+ ALLOY,
+ ALLOY_2,
+ ALONSO,
+ ALVARO,
+ AMAL,
+ AMALA,
+ AMANDA,
+ AMANY,
+ AMBER,
+ AMBRE,
+ AMEHA,
+ AMINA,
+ AMY,
+ AMY_2,
+ AMY_3,
+ ANA,
+ ANAHIT,
+ ANANYA,
+ ANANYA_2,
+ ANBU,
+ ANDER,
+ ANDRE,
+ ANDRE_2,
+ ANDREA,
+ ANDRES,
+ ANDREW,
+ ANDREW_2,
+ ANDREW_3,
+ ANDREW_4,
+ ANGELO,
+ ANILA,
+ ANNETTE,
+ ANTOINE,
+ ANTONIN,
+ ANTONIO,
+ ANU,
+ AOI,
+ ARABELLA,
+ ARDI,
+ ARIA,
+ ARIA_2,
+ ARIANE,
+ ARJUN,
+ ARJUN_2,
+ ARJUN_3,
+ ARLET,
+ ARNAU,
+ ARNAUD,
+ ARTHUR,
+ ARYA,
+ ARYA_2,
+ ASAD,
+ ASHLEY,
+ ASILIA,
+ ASTRID,
+ ATHINA,
+ AVA,
+ AVA_2,
+ AVA_3,
+ AVRI,
+ AYANDA,
+ AYANDA_2,
+ AYSHA,
+ BABEK,
+ BANU,
+ BASHKAR,
+ BASSEL,
+ BATAA,
+ BEATRICE,
+ BEATRIZ,
+ BELKYS,
+ BELLA,
+ BENIGNO,
+ BERND,
+ BIANCA,
+ BIANCA_2,
+ BIANCA_3,
+ BLESSICA,
+ BONG_JIN,
+ BORISLAV,
+ BRANDON,
+ BRANDON_2,
+ BREE,
+ BRENDA,
+ BRIAN,
+ BRIAN_2,
+ BRIAN_3,
+ BRIGITTE,
+ BRYAN,
+ BRYAN_2,
+ BRYAN_3,
+ BURCU,
+ CALIMERO,
+ CAMILA,
+ CAMILLA,
+ CAMILLA_2,
+ CAMILLA_3,
+ CANDELA,
+ CARLA,
+ CARLOS,
+ CARLOTA,
+ CARLY,
+ CARMEN,
+ CATALDO,
+ CATALINA,
+ CECILIO,
+ CELESTE,
+ CELINE,
+ CELINE_2,
+ CHANTAL,
+ CHANTHAVONG,
+ CHARLINE,
+ CHILEMBA,
+ CHRISTEL,
+ CHRISTOPH,
+ CHRISTOPHER,
+ CHRISTOPHER_2,
+ CLARA,
+ CLAUDE,
+ COLETTE,
+ COLM,
+ CONCHITA,
+ CONNOR,
+ CONRAD,
+ CORA,
+ CORA_2,
+ CORALIE,
+ CRISTIANO,
+ DAICHI,
+ DALIA,
+ DALIA_2,
+ DANIEL,
+ DANIEL_2,
+ DANIELLE,
+ DANIELLE_2,
+ DARIO,
+ DARIYA,
+ DARREN,
+ DAUDI,
+ DAULET,
+ DAVIS,
+ DAVIS_2,
+ DAVIS_3,
+ DENA,
+ DENISE,
+ DEREK,
+ DHWANI,
+ DIEGO,
+ DILARA,
+ DIMAS,
+ DMITRY,
+ DONATO,
+ DORA,
+ DUARTE,
+ DUNCAN,
+ DUSTIN,
+ ECHO,
+ EKA,
+ ELENA,
+ ELIAS,
+ ELIMU,
+ ELIN,
+ ELIZABETH,
+ ELKE,
+ ELLIOT,
+ ELOISE,
+ ELSA,
+ ELSIE,
+ ELVIRA,
+ ELZA,
+ EMEL,
+ EMIL,
+ EMILIO,
+ EMILY,
+ EMMA,
+ EMMA_2,
+ EMMA_3,
+ EMMA_4,
+ ENRIC,
+ ENRIQUE,
+ ERIC,
+ ESTRELLA,
+ ETHAN,
+ EVELYN,
+ EVERITA,
+ EWA,
+ EWA_2,
+ EZINNE,
+ FABIO,
+ FABIOLA,
+ FABLE,
+ FABRICE,
+ FAHED,
+ FARID,
+ FATIMA,
+ FEDERICO,
+ FENNA,
+ FERNANDA,
+ FIAMMA,
+ FILIZ,
+ FINN,
+ FLORIAN,
+ FLORIAN_2,
+ FLORIN,
+ FRANCISCA,
+ FREYA,
+ GABRIELLE,
+ GABRIELLE_2,
+ GABRIJELA,
+ GADIS,
+ GAGAN,
+ GEMMA,
+ GEMMA_2,
+ GERAINT,
+ GERARD,
+ GERARDO,
+ GIANNI,
+ GIORGI,
+ GIORGIO,
+ GIOVANNA,
+ GISELA,
+ GIUSEPPE,
+ GIUSEPPE_2,
+ GONZALO,
+ GOOK_MIN,
+ GORAN,
+ GRACE,
+ GREGORY,
+ GUDRUN,
+ GUL,
+ GUL_NAWAZ,
+ GUNNAR,
+ GUY,
+ GWYNETH,
+ HALA,
+ HAMDAN,
+ HAMED,
+ HANNAH,
+ HANNAH_2,
+ HANS,
+ HARRI,
+ HAYK,
+ HEDI,
+ HEMKALA,
+ HENRI,
+ HILA,
+ HILLEVI,
+ HIU_GAAI,
+ HIUJIN,
+ HIU_MAAN,
+ HOAI_MY,
+ HOLLIE,
+ HSIAO_CHEN,
+ HSIAO_YU,
+ HUMBERTO,
+ HYUNSU,
+ HYUNSU_2,
+ HYUNSU_3,
+ IDA,
+ ILIR,
+ IMAN,
+ IMANI,
+ IMELDA,
+ INES,
+ INES_2,
+ INGRID,
+ IN_JOON,
+ IRENE,
+ IRMA,
+ ISABELLA,
+ ISABELLA_2,
+ ISABELLA_3,
+ ISABELLE,
+ ISABELLE_2,
+ ISELIN,
+ ISIDORA,
+ ISMAEL,
+ IVY,
+ IVY_2,
+ JACEK,
+ JACOB,
+ JACQUELINE,
+ JAJANG,
+ JAMAL,
+ JAMES,
+ JAN,
+ JANE,
+ JANE_2,
+ JANN,
+ JASMINE,
+ JASMINE_2,
+ JASON,
+ JAVIER,
+ JEAN,
+ JENNY,
+ JENNY_2,
+ JENNY_3,
+ JEPPE,
+ JEROME,
+ JIHYE,
+ JI_MIN,
+ JIMMIE,
+ JITKA,
+ JOANA,
+ JOANNA,
+ JOANNA_2,
+ JOANNA_3,
+ JOANNE,
+ JOEY,
+ JOEY_2,
+ JONAS,
+ JORGE,
+ JORGE_2,
+ JOSEPH,
+ JOSEPHINE,
+ JUAN,
+ JULIO,
+ JUSTIN,
+ JUSTIN_2,
+ KAI,
+ KAJAL,
+ KAJAL_2,
+ KALINA,
+ KANI,
+ KARINA,
+ KARL,
+ KARLA,
+ KASPER,
+ KATJA,
+ KAVYA,
+ KAVYA_2,
+ KAZUHA,
+ KEITA,
+ KEN,
+ KENDRA,
+ KENDRA_2,
+ KEOMANY,
+ KERT,
+ KEVIN,
+ KILLIAN,
+ KIM,
+ KIMBERLY,
+ KIMBERLY_2,
+ KLARISSA,
+ KLAUS,
+ KUMAR,
+ KUNAL,
+ KUNAL_2,
+ LAIA,
+ LAILA,
+ LAITH,
+ LARISSA,
+ LATIFA,
+ LAURA,
+ LAURA_2,
+ LAYLA,
+ LEA,
+ LEA_2,
+ LEA_3,
+ LEAH,
+ LEILA,
+ LENI,
+ LENNART,
+ LEONAS,
+ LETICIA,
+ LEWIS,
+ LIA,
+ LIAM,
+ LIAN,
+ LIAN_2,
+ LIBBY,
+ LIBERTO,
+ LISA,
+ LISA_2,
+ LISANDRO,
+ LIV,
+ LIVIA,
+ LIVIA_2,
+ LOLA,
+ LORENA,
+ LORENZIO,
+ LORENZO,
+ LOTTE,
+ LOUISA,
+ LUCIA,
+ LUCIA_2,
+ LUCIA_3,
+ LUCIANO,
+ LUCIEN,
+ LUIS,
+ LUKAS,
+ LUKE,
+ LUNA,
+ LUNA_2,
+ LUPE,
+ LUPE_2,
+ LUPE_3,
+ MAARTEN,
+ MACERIO,
+ MACERIO_2,
+ MADHUR,
+ MADINA,
+ MADS,
+ MAISIE,
+ MAJA,
+ MANOHAR,
+ MANUEL,
+ MANUELA,
+ MARCELLO,
+ MARCELO,
+ MAREK,
+ MARGARITA,
+ MARIA,
+ MARIJA,
+ MARINA,
+ MARIO,
+ MARLENE,
+ MARTA,
+ MARYAM,
+ MASARU,
+ MATEO,
+ MATHIEU,
+ MATTHEW,
+ MATTHEW_2,
+ MATTHEW_3,
+ MATTIAS,
+ MAURICE,
+ MAXIM,
+ MAYA,
+ MAYU,
+ MEKDES,
+ MIA,
+ MIA_2,
+ MIA_3,
+ MICHELLE,
+ MIDHUN,
+ MIGUEL,
+ MITCHELL,
+ MIZUKI,
+ MOAZ,
+ MOHAN,
+ MOLLY,
+ MONICA,
+ MOUNA,
+ MUUSE,
+ NABANITA,
+ NAJA,
+ NAM_MINH,
+ NANAMI,
+ NANAMI_2,
+ NANCY,
+ NANCY_2,
+ NAOKI,
+ NATASHA,
+ NEERJA,
+ NEERJA_2,
+ NEIL,
+ NESTORAS,
+ NIA,
+ NIAMH,
+ NIAMH_2,
+ NICHOLAS,
+ NICHOLAS_2,
+ NICOLAU,
+ NICOLE,
+ NIL,
+ NILAR,
+ NILS,
+ NIRANJAN,
+ NIWAT,
+ NOAH,
+ NOEMI,
+ NOORA,
+ NOURA,
+ NOVA,
+ NOVA_2,
+ NURIA,
+ OJAS,
+ OLA,
+ OLA_2,
+ OLIVER,
+ OLIVIA,
+ OLLIE,
+ OLLIE_2,
+ OMAR,
+ ONA,
+ ONYX,
+ ORLA,
+ OSMAN,
+ OSTAP,
+ PALLAVI,
+ PALMIRA,
+ PALOMA,
+ PAOLA,
+ PEDRO,
+ PEDRO_2,
+ PELAYO,
+ PENELOPE,
+ PERNILLE,
+ PETRA,
+ PHOEBE,
+ PHOEBE_2,
+ PIERINA,
+ PISETH,
+ POLINA,
+ PRABHAT,
+ PRABHAT_2,
+ PRADEEP,
+ PREMWADEE,
+ PRIYOM,
+ RAFIKI,
+ RALF,
+ RAMI,
+ RAMONA,
+ RANA,
+ RAQUEL,
+ RAVEENA,
+ REEM,
+ REHAAN,
+ REHAAN_2,
+ REHEMA,
+ REMI,
+ REMI_2,
+ REMY,
+ REMY_2,
+ RENATA,
+ RICARDO,
+ RINALDO,
+ ROBERTO,
+ RODRIGO,
+ ROGER,
+ ROI,
+ ROK,
+ ROSA,
+ RUBEN,
+ RUSSELL,
+ RUTH,
+ RUTH_2,
+ RYAN,
+ RYAN_2,
+ SABELA,
+ SABRINA,
+ SABRINA_2,
+ SAGAR,
+ SALEH,
+ SALLI,
+ SALLI_2,
+ SALLI_3,
+ SALMA,
+ SALMAN,
+ SALOME,
+ SAM,
+ SAMEERA,
+ SAMUEL,
+ SANA,
+ SAPNA,
+ SARA,
+ SARANYA,
+ SARDOR,
+ SAUL,
+ SEBASTIAN,
+ SELMA,
+ SEO_HYEON,
+ SEOYEON,
+ SEOYEON_2,
+ SEOYEON_3,
+ SERAPHINA,
+ SERAPHINA_2,
+ SERENA,
+ SERENA_2,
+ SERGIO,
+ SERGIO_2,
+ SHAKIR,
+ SHIMMER,
+ SHIORI,
+ SHRUTI,
+ SIQINIQ,
+ SIQINIQ_2,
+ SITI,
+ SOBHANA,
+ SOFFIE,
+ SOFIA,
+ SOFIE,
+ SONIA,
+ SOON_BOK,
+ SOPHIE,
+ SOPHIE_2,
+ SRECKO,
+ SREYMOM,
+ STEFFAN,
+ STEFFAN_2,
+ STEFFAN_3,
+ STEPHEN,
+ STEPHEN_2,
+ SUBHASINI,
+ SUKANT,
+ SUN_HI,
+ SUN_HI_2,
+ SURYA,
+ SUVI,
+ SVETLANA,
+ SWARA,
+ SYLVIE,
+ SYLVIE_2,
+ TAIM,
+ TAKUMI,
+ TAKUMI_2,
+ TAMAS,
+ TANIA,
+ TANISHAA,
+ TANJA,
+ TAQQIQ,
+ TAQQIQ_2,
+ TATYANA,
+ TEO,
+ TERESA,
+ THALITA,
+ THALITA_2,
+ THALITA_3,
+ THANDO,
+ THEMBA,
+ THIAGO,
+ THIERRY,
+ THIERRY_2,
+ THIHA,
+ THILINI,
+ THOMAS,
+ TIANA,
+ TIFFANY,
+ TIM,
+ TINA,
+ TOMAS,
+ TOMOKO,
+ TONY,
+ TRIANA,
+ TRISTAN,
+ TRISTAN_ES,
+ TRISTAN_ES_2,
+ TUTI,
+ TYLER,
+ UBAX,
+ UZMA,
+ VAANI,
+ VALENTINA,
+ VALERIO,
+ VALLUVAR,
+ VENBA,
+ VERA,
+ VESNA,
+ VICKI,
+ VICKI_2,
+ VICKI_3,
+ VICTOR,
+ VIKTORIA,
+ VITORIA,
+ VITORIA_2,
+ VIVIENNE,
+ VIVIENNE_2,
+ VLASTA,
+ WAN_LUNG,
+ WAYNE,
+ WILLEM,
+ WILLIAM,
+ WILLIAM_2,
+ XIAOCHEN,
+ XIAOCHEN_2,
+ XIAOCHEN_3,
+ XIAOCHEN_4,
+ XIAOHAN,
+ XIAOHAN_2,
+ XIAOMENG,
+ XIAO_MIN,
+ XIAOMO,
+ XIAOQIU,
+ XIAOROU,
+ XIAORUI,
+ XIAOSHUANG,
+ XIAOSHUANG_2,
+ XIAOSHUANG_3,
+ XIAOTONG,
+ XIAOXIAO,
+ XIAOXIAO_2,
+ XIAOXIAO_3,
+ XIAOXIAO_4,
+ XIAOXIAO_5,
+ XIAOYAN,
+ XIAOYI,
+ XIAOYI_2,
+ XIAOYOU,
+ XIAOYOU_2,
+ XIAOYOU_3,
+ XIAOYU,
+ XIAOYU_2,
+ XIAOZHEN,
+ XIMENA,
+ XIMENA_ES,
+ XIMENA_ES_2,
+ XIMENA_ES_3,
+ YAGO,
+ YAN,
+ YARA,
+ YASHICA,
+ YASMIN,
+ YESUI,
+ YOLANDA,
+ YU_JIN,
+ YUNDENG,
+ YUNFAN,
+ YUNFAN_2,
+ YUNFENG,
+ YUNHAN,
+ YUNHAO,
+ YUN_JHE,
+ YUNJIAN,
+ YUNJIE,
+ YUN_SONG,
+ YUNXI,
+ YUNXI_2,
+ YUNXIA,
+ YUNXIA_2,
+ YUNXIANG,
+ YUNXIAO,
+ YUNXIAO_2,
+ YUNYANG,
+ YUNYE,
+ YUNYE_2,
+ YUNYI,
+ YUNYI_2,
+ YUNZE,
+ YUNZHE,
+ YVES,
+ YVETTE,
+ ZARIYAH,
+ ZAYD,
+ ZEINA,
+ ZHIYU,
+ ZHIYU_2,
+ ZOFIA,
+ ZURI));
+
+ private VoiceName(String value) {
+ super(value);
+ }
+
+ public static Stream values() {
+ return ENUM_SUPPORT.values();
+ }
+
+ public static VoiceName from(String value) {
+ return ENUM_SUPPORT.from(value);
+ }
+
+ public static String valueOf(VoiceName e) {
+ return ENUM_SUPPORT.valueOf(e);
+ }
+}
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java
index e3b9ac391..646293d43 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -90,6 +91,17 @@ interface Builder {
*/
Builder setCommands(List commands);
+ /**
+ * Set the commands from one or more commands, e.g. a single one
+ *
+ * @param commands Commands, in the order they run
+ * @return Current builder
+ * @see #setCommands(List)
+ */
+ default Builder setCommands(SvamlCommand... commands) {
+ return setCommands(Arrays.asList(commands));
+ }
+
/**
* see getter
*
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java
index 3bd36b73a..ab2e711a1 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Arrays;
import java.util.List;
/** Request body for patching an ongoing call with SVAML commands. */
@@ -64,6 +65,17 @@ interface Builder {
*/
Builder setCommands(List commands);
+ /**
+ * Set the commands from one or more commands, e.g. a single one
+ *
+ * @param commands Commands, in the order they run
+ * @return Current builder
+ * @see #setCommands(List)
+ */
+ default Builder setCommands(SvamlCommand... commands) {
+ return setCommands(Arrays.asList(commands));
+ }
+
/**
* see getter
*
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java
index 9cfcfd75a..cd4812f91 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Arrays;
import java.util.List;
/** Request to start a single outbound call. */
@@ -64,6 +65,17 @@ interface Builder {
*/
Builder setCommands(List commands);
+ /**
+ * Set the commands from one or more commands, e.g. a single one
+ *
+ * @param commands Commands, in the order they run
+ * @return Current builder
+ * @see #setCommands(List)
+ */
+ default Builder setCommands(SvamlCommand... commands) {
+ return setCommands(Arrays.asList(commands));
+ }
+
/**
* see getter
*
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java
index 126ccff67..74936971b 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEvents;
import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import java.util.Arrays;
import java.util.List;
/**
@@ -78,6 +79,17 @@ interface Builder {
*/
Builder setCommands(List commands);
+ /**
+ * Set the commands from one or more commands, e.g. a single one
+ *
+ * @param commands Commands, in the order they run
+ * @return Current builder
+ * @see #setCommands(List)
+ */
+ default Builder setCommands(SvamlCommand... commands) {
+ return setCommands(Arrays.asList(commands));
+ }
+
/**
* see getter
*
diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java
index fb5757a94..12a70f2ed 100644
--- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java
+++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java
@@ -1,6 +1,7 @@
package com.sinch.sdk.domains.voice.models.v2.svaml;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.util.Arrays;
import java.util.List;
/**
@@ -67,6 +68,17 @@ interface Builder {
*/
Builder setCommands(List commands);
+ /**
+ * Set the commands from one or more commands, e.g. a single one
+ *
+ * @param commands Commands, in the order they run
+ * @return Current builder
+ * @see #setCommands(List)
+ */
+ default Builder setCommands(SvamlCommand... commands) {
+ return setCommands(Arrays.asList(commands));
+ }
+
/**
* see getter
*
diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/SingleCommandBuilderTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/SingleCommandBuilderTest.java
new file mode 100644
index 000000000..e5584af79
--- /dev/null
+++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/SingleCommandBuilderTest.java
@@ -0,0 +1,82 @@
+package com.sinch.sdk.domains.voice.models.v2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sinch.sdk.core.utils.databind.Mapper;
+import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest;
+import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest;
+import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest;
+import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.AnswerCommand;
+import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** The Voice V2 models holding SVAML commands accept one or more commands, as well as a list */
+class SingleCommandBuilderTest {
+
+ static final SvamlCommand HANGUP = HangupCommand.HANGUP_COMMAND;
+ static final List ONE = Collections.singletonList(HANGUP);
+ static final List TWO = Arrays.asList(AnswerCommand.ANSWER_COMMAND, HANGUP);
+
+ @Test
+ void startCallRequest() {
+ Assertions.assertThat(StartCallRequest.builder().setCommands(HANGUP).build())
+ .isEqualTo(StartCallRequest.builder().setCommands(ONE).build());
+ Assertions.assertThat(
+ StartCallRequest.builder()
+ .setCommands(AnswerCommand.ANSWER_COMMAND, HANGUP)
+ .build()
+ .getCommands())
+ .isEqualTo(TWO);
+ }
+
+ @Test
+ void startBatchRequest() {
+ Assertions.assertThat(StartBatchRequest.builder().setCommands(HANGUP).build())
+ .isEqualTo(StartBatchRequest.builder().setCommands(ONE).build());
+ }
+
+ @Test
+ void callPatchRequest() {
+ Assertions.assertThat(CallPatchRequest.builder().setCommands(HANGUP).build())
+ .isEqualTo(CallPatchRequest.builder().setCommands(ONE).build());
+ }
+
+ @Test
+ void svamlInput() {
+ Assertions.assertThat(SvamlInput.builder().setCommands(HANGUP).build())
+ .isEqualTo(SvamlInput.builder().setCommands(ONE).build());
+ }
+
+ @Test
+ void voiceSinchEventResponse() {
+ Assertions.assertThat(VoiceSinchEventResponse.builder().setCommands(HANGUP).build())
+ .isEqualTo(VoiceSinchEventResponse.builder().setCommands(ONE).build());
+ }
+
+ /** Jackson still deserializes the builders: the overload is not a second setter for it */
+ @Test
+ void deserializationIsUnchanged() throws Exception {
+ ObjectMapper mapper = Mapper.getInstance();
+ for (Object model :
+ Arrays.asList(
+ StartCallRequest.builder().setCommands(HANGUP).build(),
+ StartBatchRequest.builder()
+ .setCommands(HANGUP)
+ .setParameters(Collections.singletonList(Collections.singletonMap("to", "+1")))
+ .build(),
+ CallPatchRequest.builder().setCommands(HANGUP).build(),
+ SvamlInput.builder().setCommands(HANGUP).build(),
+ VoiceSinchEventResponse.builder().setCommands(HANGUP).build())) {
+ String json = mapper.writeValueAsString(model);
+ Object read = mapper.readValue(json, model.getClass().getInterfaces()[0]);
+
+ Assertions.assertThat(mapper.writeValueAsString(read)).isEqualTo(json);
+ }
+ }
+}
diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceNameTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceNameTest.java
new file mode 100644
index 000000000..6e04fcb68
--- /dev/null
+++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceNameTest.java
@@ -0,0 +1,32 @@
+package com.sinch.sdk.domains.voice.models.v2;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class VoiceNameTest {
+
+ @Test
+ void declaresEveryVoiceOfTheSpec() {
+ Assertions.assertThat(VoiceName.values()).hasSize(772);
+ }
+
+ @Test
+ void constantsKeepTheWireValue() {
+ Assertions.assertThat(VoiceName.valueOf(VoiceName.EMMA)).isEqualTo("Emma");
+ Assertions.assertThat(VoiceName.valueOf(VoiceName.AARAV_2)).isEqualTo("Aarav_2");
+ Assertions.assertThat(VoiceName.valueOf(VoiceName.BONG_JIN)).isEqualTo("BongJin");
+ Assertions.assertThat(VoiceName.valueOf(VoiceName.SUN_HI_2)).isEqualTo("SunHi_2");
+ }
+
+ @Test
+ void fromReturnsTheDeclaredConstant() {
+ Assertions.assertThat(VoiceName.from("Emma_3")).isSameAs(VoiceName.EMMA_3);
+ }
+
+ @Test
+ void fromAcceptsAVoiceOutsideTheCatalog() {
+ VoiceName future = VoiceName.from("FutureVoice");
+
+ Assertions.assertThat(VoiceName.valueOf(future)).isEqualTo("FutureVoice");
+ }
+}
diff --git a/pom.xml b/pom.xml
index 87a92996e..a860fed31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.sinch.sdk
sinch-sdk-java
- 2.4.0-SNAPSHOT
+ 2.5.0-SNAPSHOT
Sinch Java SDK