Synthesising speech
This time the code is a little more complete. I’ve wrapped it in a function and shown the if statement that branches based off the platform.
import OpenAI from "openai";
import { Audio } from 'expo-av';
import { Platform } from "react-native";
import * as FileSystem from 'expo-file-system';
import { Buffer } from 'buffer';
////////////////////////////////////////////////////////////
const apiKey = process.env.EXPO_PUBLIC_OPENAI_API_KEY;
const openai = new OpenAI({ apiKey: apiKey, dangerouslyAllowBrowser: true });
////////////////////////////////////////////////////////////
export async function synthesizeSpeech(text: string): Promise<void> {
try {
// This asks openAI to convert to the text to audio
const response = await openai.audio.speech.create({
model: "tts-1",
voice: "alloy",
input: text,
});
// We have to treat web and Android differently
if (Platform.OS === 'web') {
const audioBlob = new Blob([await response.arrayBuffer()], {
type: 'audio/mp3'
});
const audioUri = URL.createObjectURL(audioBlob);
const { sound } = await Audio.Sound.createAsync({ uri: audioUri });
await sound.playAsync();
} else {
// I tested this on Android, but it may be the same for iOS
// Download the audio data first
const audioData = await response.arrayBuffer();
const base64Audio = Buffer.from(audioData).toString('base64');
// Save to a temporary file
const fileUri = `${FileSystem.documentDirectory}temp-audio.mp3`;
await FileSystem.writeAsStringAsync(fileUri, base64Audio, {
encoding: FileSystem.EncodingType.Base64,
});
// Play the local file
const { sound } = await Audio.Sound.createAsync(
{ uri: fileUri },
{ shouldPlay: false }
);
await sound.playAsync();
}
} catch (error) {
console.error('Error in synthesizeSpeech:', error);
throw error;
}
}
A Buffer gotcha (Or got me, anyway)
Note that Buffer is imported. You typing system probably won’t show an error if you don’t, but you need to npm install buffer and import it at the top of the file for mobile.
Some of Cursors suggested alternatives
Using the URL from the response data
Cursor suggested this and I thought it had become way easier. The response data provides a URL!!! Unfortunately, this means that you’re trying to download the audio file directly from OpenAI’s servers, which, even if you provide your API Key, isn’t allowed.
// Can't do this because it tries to download from the open AI servers
const { sound } = await Audio.Sound.createAsync(
{
uri: response.url,
// Even with this it's not allowed
// headers: { 'Authorization': `Bearer ${apiKey}` }
},
{ shouldPlay: false }
);
await sound.playAsync();
Using response.blob
When Cursor told me I couldn’t use arrayBuffer on Android, it suggested using the blob straight from the response. Unfortunately, you still need to turn it into a URI, and createObjectURL can’t be used either.
// This won't work either, because createObjectURL can't be used on Android.
const audioData = await response.blob();
const audioUri = URL.createObjectURL(audioData);
const { sound } = await Audio.Sound.createAsync({ uri: audioUri });
await sound.playAsync();
There may be cleaner was to do this, but while figuring this out, I couldn’t find any useful information online, so hopefully this post helps someone else.