HomeyScript with Elevenlabs AI-voice to Sonos help

Hi,

I’ve been bashing my head against a wall for some days now trying to figure out how one could make a Homey Script that can read out my ‘good morning message’ via ElevenLabs API (send input to elevenlabs then get an url which my Sonos should play). Thus far I’ve been able to create a script that sends a string to ElevenLabs API, and then recieves the audio, but in order to make it work on Sonos i need a URL, thus, i’ve used file.io to temporarily upload the audio via their API. Here is where I’m essentially stuck, because when the file gets uploaded to file.io, it is indeed an .mp3 file, but it does not play and seems to be corrupt. Also, there was a big hassle to figure out how to decode the data i got from the elevenlabs API, blob does not work in homeyscript.

A big caveat, I’ve attempted this together with ChatGPT, thus, my coding experience might not be on par with my mission. Any further guidance would be highly appreciated.

const apiKey = '';
const text = 'Hello there!';
const voiceId = '';
const fileIoApiKey = ''; 

/// Funktion för att skicka POST-begäran till ElevenLabs TTS API och hämta ljuddata
async function getAudioData(text, voiceId) {
    console.log("Sending request to ElevenLabs API...");
    const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
        method: 'POST',
        headers: {
            'xi-api-key': apiKey,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            text: text,
            voice_settings: {
                stability: 0.5,
                similarity_boost: 0.5
            }
        })
    });

    if (!response.ok) {
        const errorData = await response.text();
        console.error(`HTTP error! Status: ${response.status}, Details: ${errorData}`);
        throw new Error(`HTTP error! Status: ${response.status}, Details: ${errorData}`);
    }

    const arrayBuffer = await response.arrayBuffer();
    console.log("Received audio data from ElevenLabs API...");
    console.log(`Audio data length: ${arrayBuffer.byteLength}`);
    return arrayBuffer;
}

// Funktion för att konvertera ArrayBuffer till en Base64-sträng utan `btoa`
function arrayBufferToBase64(buffer) {
    const uint8Array = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < uint8Array.byteLength; i++) {
        binary += String.fromCharCode(uint8Array[i]);
    }
    return Buffer.from(binary, 'binary').toString('base64');
}

// Funktion för att ladda upp rå binärdata till file.io och få en URL
async function uploadAudioData(audioBuffer) {
    console.log("Uploading audio data to file.io...");
    
    const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
    let body = `--${boundary}\r\n`;
    body += `Content-Disposition: form-data; name="file"; filename="audio.mp3"\r\n`;
    body += 'Content-Type: audio/mpeg\r\n\r\n';
    body += arrayBufferToBase64(audioBuffer);
    body += `\r\n--${boundary}--\r\n`;

    const response = await fetch('https://file.io', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${fileIoApiKey}`,
            'Content-Type': `multipart/form-data; boundary=${boundary}`
        },
        body: body
    });

    if (!response.ok) {
        const errorData = await response.text();
        console.error(`File upload error! Status: ${response.status}, Details: ${errorData}`);
        throw new Error(`File upload error! Status: ${response.status}, Details: ${errorData}`);
    }

    const data = await response.json();
    console.log("Uploaded audio data to file.io. URL:", data.link);
    return data.link; // file.io returns the URL in the 'link' property
}

// Huvudfunktion för att hantera processen
async function main() {
    try {
        const audioBuffer = await getAudioData(text, voiceId);

        // Kontrollera att vi har fått korrekt ljuddata
        if (!audioBuffer || audioBuffer.byteLength === 0) {
            console.error('Received empty audio data');
            throw new Error('Received empty audio data');
        }

        const audioUrl = await uploadAudioData(audioBuffer);
        console.log(`Generated Audio URL: ${audioUrl}`);
        return audioUrl;
    } catch (error) {
        console.error(error);
        return `An error occurred: ${error.message}`;
    }
}

// Kör huvudfunktionen och returnera resultatet
return await main();

I shared my prompt to build a script here (link)

1 Like

Hi Wout, cheers for the help.

I don’t think that is the problem in this case though, the request to the API is successful, the problem is rather how I can deal with the base64 data that the script is trying to store on a server in order to get a valid url for the speakers. I can store the url in either a variable or a tag, but it still does not answer why my mp3 file becomes corrupt and not playable on my speakers unfortunately. :frowning:

I might have misunderstood something but that’s where I’m at right now.

Once again, cheers for the answer

hmmmm well done on the script part with GPT… on the url part, no suggestions … unless also insert errors to GPT to try to fix it :frowning: