0

I am working on a static (nextjs) webapp on azure. I want to record my voice (like 5 to 10sec) and get a transcript of it using openai. I got the code working when I send a form with audio data with postman.

But in azure static web app (swa) there are some challenges. when I send a form with an audio file to the AZ function app, it cant extract the audio or even get a hold of the formData. It seems like when trying to get the formData, the function app just freezes and dont any show errors.

I did some research and think the issue is that the form being send needs to get parsed. I tried a couple of packages, like: formidable, multiparty and @anzp/azure-function-multipart.

When I use Formidable package to parse, for some reason I cant import any exported method and use any. I imported it as following : " import FormidableMeth,{parse} from "formidable". When I click on the "formidable" it leads to node_modules/formidable/dist/index.cjs. It doesnt seem like a package I can import from because it uses a different syntax like "require(), var".

The last one (package @anzp/azure-function-multipart) seems to be fit for AZ function app (obviously beacuse of the name :p), but it hasn't been updated in a couple of years and it uses a outdated "@azure/[email protected]" and I have "@azure/[email protected]". I tried using a forked version forked_azure_function_multipart which has been updated like a month ago but when I checked the package.json of the forked version I still see an older version being installed in the function app. And I see an error : "Type 'HttpRequest' is missing the following properties from type 'HttpRequest': get, parseFormBody"

I tried and use "patch package" and update the package.json, but this did not work with fixxing the issue.

I read some forums but I couldnt find a solution e.g. read stack overflow forum.

Does anyone has a idea how I could parse the formData and extract the audiofile and store it in a folder (for testing purpose before storing it in az storage) in azure function app using a static webapp made of NextJS.

example (SWA function) code frontend:

async function formAction(event: any) {

     const NewForm = new FormData();
     NewForm.append('audio', event.get('audio'), 'audio.webm');
     console.log({auidFormData: event.get('audio'), event: NewForm});

     const awaitTranscript = await fetch("/api/voiceTranscriptTrigger",{method:"POST", body: NewForm, headers: {
            "Content-Type": "multipart/form-data"
     } });
        
     if(awaitTranscript.ok){
       const response = await awaitTranscript.json(); 
       console.log({response_awaitTranscript: response});
     }
}

example code (backend) Function app (extracting the formData):

import parseMultipartFormData from "@anzp/azure-function-multipart"; 

export async function voiceTranscriptTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    
try {
     console.log(`==> before checking request : <==`);
     const { fields, files } = await parseMultipartFormData(request);
     console.log({fields: fields});
     console.log({files: files});
     return new HttpResponse(test);

} catch (e) {
      console.error("Error while trying to upload a file\n", e);
      return new HttpResponse({body: JSON.stringify({"error": "Something went wrong."})});
    }
};

app.http('voiceTranscriptTrigger', {
    methods: ['POST'],
    authLevel: 'anonymous',
    handler: voiceTranscriptTrigger
});

In advance I want to thank you for reading this and even your thought of helping me solve this issue I appreciate.

Kind Regards

=============== UPDATE ===============

I have been trying out busboy package to parse the form data containing the audiofile.

Link to my github repository containg a demo app and the AZ function app is located in the api folder.

I am getting an error while trying to use the function app (request.body.)pipeTo method according to the following article, the error I receive is "typeError [ERR_INVALID_STATE]: Invalid state: The ReadableStream is locked"

Here is the full feedback log response when I try and parse the audiofile

=========== Update V2 =============

I have updated github repository voiceTranscriptTrigger file of my demo app. With Busboy I am able to write a file within the api folder but it does not contain the full audio file, when I try and play the created file I get an error : test.webm:1 Failed to load resource: net::ERR_REQUEST_RANGE_NOT_SATISFIABLE

11
  • you can use npm i busboy/npm i parse-multipart Commented Jan 28 at 3:31
  • @Sampath Thank you for your suggestion I spend like the entire morning trying busboy and parse-multi-part. I have a setup with busboy but encounter an error while trying to use request.body.pipeTo() method, like : "property must be of instance WriteableStream" / "TypeError: Body is unusable at specConsumeBody" . Here is what I got after a couple of ours trying busboy, I am not a top tier developer (yet), so I still trying to understand how it works see my code and dont pay to much attention to the weird function name Commented Jan 28 at 12:24
  • @Sampath I tried the pipeTo method according to the following (while trying busboy) page but got an error "TypeError [ERR_INVALID_STATE]: Invalid state: The ReadableStream is locked". see code Commented Jan 29 at 3:36
  • Thank you for your help so far, it helped a lot of my understanding of working with Az function app. Commented Jan 29 at 11:05
  • Refer this doc for Create a function in Azure with TypeScript using Visual Studio Code with v4 Commented Jan 29 at 11:20

2 Answers 2

0

Below is the code to parse FormData containing an audio file in an Azure Function App:


import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import parseMultipartFormData from "@anzp/azure-function-multipart";
import * as fs from 'fs';
import * as path from 'path';

const voiceTranscriptTrigger: AzureFunction = async function (
    context: Context,
    req: HttpRequest
): Promise<void> {
    try {
        console.log("==> Before parsing multipart data <==");
        const { fields, files } = await parseMultipartFormData(req);

        console.log("==> Parsed fields: ", fields);
        console.log("==> Parsed files: ", files);

        if (files && files.length > 0) {
     
            const audioFile = files[0]; 
            console.log("Received file: ", audioFile.filename);

            const outputFolder = path.join(__dirname, 'uploadedFiles');
            if (!fs.existsSync(outputFolder)) {
                fs.mkdirSync(outputFolder); 
            }
            const filePath = path.join(outputFolder, audioFile.filename);
            fs.writeFileSync(filePath, audioFile.bufferFile);

            console.log(`File saved at: ${filePath}`);
            context.res = {
                status: 200,
                body: {
                    message: 'File uploaded successfully',
                    filename: audioFile.filename,
                    path: filePath
                }
            };
        } else {

            context.res = {
                status: 400,
                body: { error: "No audio file uploaded" }
            };
        }
    } catch (e) {
        console.error("Error while processing file upload:", e);
        context.res = {
            status: 500,
            body: { error: "Something went wrong while processing the file" }
        };
    }
};

export default voiceTranscriptTrigger;

Output: enter image description here

enter image description here

enter image description here

Frontend Code:

async function formAction(event: any) {
    const NewForm = new FormData();
    NewForm.append('audio', event.get('audio'), 'audio.webm'); 
    console.log({ auidFormData: event.get('audio'), event: NewForm });

    const awaitTranscript = await fetch("http://localhost:7071/api/HttpTrigger1", {
        method: "POST",
        body: NewForm,
        headers: {
            
        }
    });

    if (awaitTranscript.ok) {
        const response = await awaitTranscript.json();
        console.log({ response_awaitTranscript: response });
    }
}

To integrate the Function App code with the frontend code, make sure to add CORS. You can refer to my SO post to learn how to add CORS configuration.

Sign up to request clarification or add additional context in comments.

5 Comments

THank you for offering a solution.. I unfortunatley encounter some error like, I am not able to import AzureFunction doenst exist in the current version @azure/functions package I got (4.6.0), also I am getting another error like: " Type 'HttpRequest' is missing the following properties from type 'HttpRequest': get, parseFormBody" -from-> await parseMultipartFormData(request) ... I am looking what else I can do to maybe fix this current solution in my static webapp.. THank you very much
Use npm install @azure/functions@latest to install and Install the Azure Functions Core Tools
Thank you for your shift reply .. my vs code shows me this after updating as you mentioned : "Module '"@azure/functions"' has no exported member 'AzureFunction'" .. I tried uninstalling it and re-installing and shows the same notification
I see your using V3 of Azure function according to this page. is there a equivalent of AzureFunction in V4, I tried adding "get" in the method of the trigger but dit not help.
Also when trying to create a v3 az function app from my vs code, I get the following message preventing me from creating a v3 function app in vs code No stacks found for Azure Functions v3 due to being EOL. Learn how to upgrade to V4, so I am going to continue trying it with busboy as you mentioned in the comments.
0

@Sampath this code below works for me in azure function v4 using busboy.

currently it stores the file in the api folder of the function app.

If I helped someone with this give this answer a like :D.

async function voiceparser(request: HttpRequest, context) {
            const bodyPipe = await request.body;
            
            function readHeader(request: HttpRequest, key: string) {
                return {[key] :Object.fromEntries(request.headers.entries())[key]}; // + `; boundary=${boundary}`
            }

            return new Promise((resolve, reject) => {
            
                console.log({contentType: request.headers.values});              
                console.log({new_header: readHeader(request, 'content-type')});
                
                const busboyHeader = readHeader(request, 'content-type');
                const bb = busboy({ headers: busboyHeader });
                
                const saveTo = join(process.cwd(), "./test.webm",);upload-${random()}`);
                const downloadWriteStream = createWriteStream(saveTo);
                
                console.log({temp_directory: saveTo});

                bb.on('file', (fieldname, file, filename, encoding, mimetype) => {
                   const saveTo = join(process.cwd(), './audio.webm', filename);
                    const writeStream = createWriteStream(saveTo);
                    file.on("data", (chunk) => {
                        if(typeof chunk === 'object');
                        writeStream.write(chunk);
                    });
              
                  });

                  console.log({readableStream: bodyPipe});
                  bodyPipe.pipeTo(Writable.toWeb(bb));
            })
        }

        voiceparser(request, context);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.