Group Abstract Group Abstract

Message Boards Message Boards

0
|
267 Views
|
0 Replies
|
0 Total Likes
View groups...
Share
Share this post:

Calling a text-to-speech API from Wolfram Language

Posted 20 days ago

I wanted a small, reusable Wolfram Language function that could call a speech API, decode the returned PCM data, and produce an Audio object without writing an intermediate file. The example below uses a text to speech service with a JSON API, but the same pattern applies to any endpoint that returns base64-encoded linear PCM.

Request and response shape

The request sends text, the original text, and one or more voice assignments. Authentication is a Bearer token. The response is JSON rather than an MP3 download; its data object contains audioBase64, mimeType, sampleRate, numChannels, and bitsPerSample.

A minimal request body looks like this:

<|
  "text" -> "Welcome. This sentence was generated from Wolfram Language.",
  "originalText" -> "Welcome. This sentence was generated from Wolfram Language.",
  "speakers" -> {<|"voiceName" -> "Kore"|>}
|>

A reusable Wolfram Language function

Store the API key in the FLOWSPEECH_API_KEY environment variable before starting the kernel. Keeping it outside the notebook prevents an accidental credential leak when the notebook is shared.

ClearAll[flowSpeechAudio];

flowSpeechAudio[text_String, voice_String : "Kore"] := Module[
  {apiKey, endpoint, payload, response, json, data, bytes, unsigned, signed},

  apiKey = Environment["FLOWSPEECH_API_KEY"];
  If[! StringQ[apiKey] || StringLength[apiKey] == 0,
    Return @ Failure[
      "MissingAPIKey",
      <|"MessageTemplate" ->
        "Set the FLOWSPEECH_API_KEY environment variable before calling the function."|>
    ]
  ];

  endpoint = URLBuild @ <|
    "Scheme" -> "https",
    "Domain" -> "flowspeech.io",
    "Path" -> {"api", "ai", "text-to-speech"}
  |>;

  payload = <|
    "text" -> text,
    "originalText" -> text,
    "speakers" -> {<|"voiceName" -> voice|>}
  |>;

  response = URLRead @ HTTPRequest[
    endpoint,
    <|
      Method -> "POST",
      "Headers" -> {
        "Authorization" -> "Bearer " <> apiKey,
        "Accept" -> "application/json",
        "Content-Type" -> "application/json"
      },
      "Body" -> ExportString[payload, "RawJSON"]
    |>
  ];

  If[response["StatusCode"] =!= 200,
    Return @ Failure[
      "HTTPError",
      <|
        "StatusCode" -> response["StatusCode"],
        "ResponseBody" -> response["Body"]
      |>
    ]
  ];

  json = ImportString[response["Body"], "RawJSON"];
  If[Lookup[json, "code", -1] =!= 0,
    Return @ Failure[
      "APIError",
      <|"Response" -> json|>
    ]
  ];

  data = json["data"];

  If[data["bitsPerSample"] =!= 16 || data["numChannels"] =!= 1,
    Return @ Failure[
      "UnsupportedAudioFormat",
      <|
        "BitsPerSample" -> data["bitsPerSample"],
        "Channels" -> data["numChannels"]
      |>
    ]
  ];

  (* audio/L16 uses signed 16-bit samples in network byte order. *)
  bytes = Normal @ BaseDecode[data["audioBase64"]];
  unsigned = FromDigits[#, 256] & /@ Partition[bytes, 2];
  signed = Replace[unsigned, n_ /; n >= 32768 :> n - 65536, {1}];

  Audio[
    N[signed/32768.0],
    SampleRate -> data["sampleRate"]
  ]
]

Calling the function returns an Audio object that can be played, plotted, analyzed, or exported like any other Wolfram Language audio expression:

audio = flowSpeechAudio[
  "Read the first sentence calmly. Then add more energy to the second sentence.",
  "Kore"
];

AudioPlot[audio]
Export["flowspeech-demo.wav", audio]

Why decode from the response metadata?

It is tempting to assume that every speech endpoint returns an MP3 or WAV file. That assumption makes integrations fragile. Here the declared MIME type is audio/L16, so the code:

  1. decodes the base64 payload,
  2. groups bytes into 16-bit big-endian samples,
  3. converts unsigned values above 32767 to signed integers,
  4. normalizes the samples to the interval used by Audio, and
  5. applies the sample rate returned by the service.

The explicit checks for bit depth and channel count are intentional. If the API later returns stereo or a different encoding, the function fails with useful metadata rather than silently producing distorted audio.

Extensions

The same helper can be extended in several useful directions:

  • accept a list of speaker-to-voice mappings for dialogue,
  • memoize results using a hash of the payload,
  • return both the Audio object and quota metadata,
  • use URLSubmit for asynchronous jobs, or
  • wrap the function in an APIFunction for a Wolfram Cloud workflow.

This pattern keeps the HTTP layer, JSON validation, and audio conversion visible, which makes it easier to debug than hiding the entire call behind a black-box import.

Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard