diff --git a/website/blog-60daysofIA/2024-04-15/managing-the-cost-of-intelligent-apps.md b/website/blog-60daysofIA/2024-04-15/managing-the-cost-of-intelligent-apps.md index d4fb10a4b6..ebc4419572 100644 --- a/website/blog-60daysofIA/2024-04-15/managing-the-cost-of-intelligent-apps.md +++ b/website/blog-60daysofIA/2024-04-15/managing-the-cost-of-intelligent-apps.md @@ -1,9 +1,9 @@ --- -date: 2024-04-15T09:00 +date: 2024-04-17T09:10 slug: managing-the-cost-of-intelligent-apps -title: "Managing the Cost of Intelligent Apps" +title: "8 Managing the Cost of Intelligent Apps" authors: [cnteam] -draft: true +draft: false hide_table_of_contents: false toc_min_heading_level: 2 toc_max_heading_level: 3 @@ -85,7 +85,7 @@ Like Azure Functions, Azure OpenAI Service offers [two plans](https://azure.micr * **Azure savings plan for compute** — A fixed hourly amount, this model accommodates fluctuations and dynamic workloads, one- or three-year commitment. :::info -Checkout the quick demo bytes for [Intelligent Apps with Azure Container Apps](https://developer.microsoft.com/en-us/reactor/series/S-1308/?wt.mc_id=blog_S-1308_webpage_reactor&ocid=buildia24_60days_blogs) for a detailed walkthrough from the product team on using open-source vector database, Qrdrant, and building a multi-LLM chat application. +Checkout the quick demo bytes for **[Intelligent Apps with Azure Container Apps](https://developer.microsoft.com/en-us/reactor/series/S-1308/?wt.mc_id=blog_S-1308_webpage_reactor&ocid=buildia24_60days_blogs)** for a detailed walkthrough from the product team on using open-source vector database, Qrdrant, and building a multi-LLM chat application. ::: #### **Azure Machine Learning** diff --git a/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-1.md b/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-1.md new file mode 100644 index 0000000000..44a4696033 --- /dev/null +++ b/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-1.md @@ -0,0 +1,441 @@ +--- +date: 2024-04-17T09:00 +slug: real-time-voice-sentiment-analysis-system-1 +title: "7.4 Real-time Voice Sentiment Analysis System 1" +authors: [cnteam] +draft: false +hide_table_of_contents: false +toc_min_heading_level: 2 +toc_max_heading_level: 3 +keywords: [Cloud, Data, AI, AI/ML, intelligent apps, cloud-native, 60-days, enterprise apps, digital experiences, app modernization, serverless, ai apps] +image: https://github.com/Azure/Cloud-Native/blob/main/website/static/img/ogImage.png +description: "In today's fast-paced digital world, understanding customer sentiment in real-time during voice calls can provide businesses with a competitive edge. This guide will show developers how to build a robust real-time voice sentiment analysis application using several key Azure services." +tags: [Build-Intelligent-Apps, 60-days-of-IA, learn-live, hack-together, community-buzz, ask-the-expert, azure-kubernetes-service, azure-functions, azure-openai, azure-container-apps, azure-cosmos-db, github-copilot, github-codespaces, github-actions] +--- + + + + + + + + + + + + + + + + + + +## Real-time Voice Sentiment Analysis System Using Azure Communication Services, Azure AI and Azure OpenAI (Part 1) + +### Introduction + +In today's fast-paced digital world, understanding customer sentiment in real-time during voice calls can provide businesses with a competitive edge. This guide will show developers how to build a robust real-time voice sentiment analysis application using several key Azure services. Specifically, we'll leverage: + +* [Azure Container Apps](https://learn.microsoft.com/azure/container-apps/overview?ocid=buildia24_60days_blogs) to deploy the backend web API +* ASP.NET Core web API backend for processing +* [Azure Communication Services](https://learn.microsoft.com/azure/communication-services/overview?ocid=buildia24_60days_blogs) for handling voice calls +* [Azure AI Language](https://learn.microsoft.com/azure/ai-services/language-service/overview?ocid=buildia24_60days_blogs) and [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview?ocid=buildia24_60days_blogs) for analyzing sentiment +* Node.js frontend for user interactions. + +#### Architecture diagram + +![a diagram of the voice sentiment analysis application architecture](../../static/img/60-days-of-ia/blogs/2024-04-17/7-4-1.png) + +#### Frontend of the sample app + +![image of the Sentiment Score in a sample application](../../static/img/60-days-of-ia/blogs/2024-04-17/7-4-2.png) + +The sample app will manage Voice over IP (VoIP) and Public Switched Telephone Network (PSTN) calling capabilities, converting speech to text on the fly, and will evaluate the sentiment of the conversation in real-time. There are many scenarios where these capabilities could be used: + +1. **Customer Support Call Centers**: Analyze customer sentiment in real-time during support calls, allowing agents to adjust their approach based on the emotional tone of the conversation. This can lead to improved customer satisfaction and faster resolution times. + +1. **Market Research Interviews**: Conduct telephonic interviews with participants and use sentiment analysis to gauge reactions to new product ideas or advertisements, providing valuable insights into market trends and consumer preferences. + +1. **Telehealth Services**: In virtual healthcare consultations, use the system to assess the sentiment of patients as they describe their symptoms or concerns. This can help healthcare providers better understand the patient's emotional state and potentially improve diagnosis and patient care. + +1. **Remote Education and E-Learning**: For online classes or training sessions, analyze the sentiment of students' responses during voice interactions to assess engagement, comprehension, and the effectiveness of the teaching material. + +1. **Financial Services**: Use in banking and financial advice call centers to detect customer sentiment during calls, identifying potential issues or opportunities for additional services based on the emotional tone of the customer. + +1. **Sales and Lead Generation**: During sales calls, analyze potential customers' sentiment to tailor the pitch dynamically, improving conversion rates by aligning with the customers' emotional responses. + +By the end of this blog, you will understand how to: + +* Set up and manage voice calling with Azure Communication Services. +* Convert speech to text using Azure Communication Service Calling Captions. +* Perform sentiment analysis on the text with both Azure AI Language and Azure Open AI Service. +* Use ASP.NET Core and Node.js to tie these services together in a seamless application. + +Let's get started. + +### Prerequisites + +#### Tools and Accounts Required: + +1. **Azure Account**: An active Azure account is required to access Azure services like Azure Communication Services, Azure AI Language, and Azure OpenAI. If you don't already have an account, you can sign up for a free trial [here](https://azure.com/free?ocid=buildia24_60days_blogs). + +1. **Visual Studio Code**: This powerful and lightweight code editor from Microsoft supports development in multiple languages including Node.js and C#. Download it from [here](https://code.visualstudio.com/). + +1. **Azure CLI**: The Azure Command Line Interface is a set of commands used to manage Azure resources directly from the terminal or command prompt. Download and installation instructions can be found [here](https://docs.microsoft.com/cli/azure/install-azure-cli?ocid=buildia24_60days_blogs). + +1. **Postman**: An API development tool that makes it easier to create, share, test, and document APIs. This is optional but recommended for testing your application’s back-end services. Download Postman from [here](https://www.postman.com/downloads/). + +1. **Node.js**: Node.js is a JavaScript runtime that allows you to build scalable network applications. It's essential for developing the front-end of our application. Download and install it from [here](https://nodejs.org/). + +1. **Docker**: Docker is a platform for developing, shipping, and running applications. We'll use it for containerizing and deploying our ASP.NET Core backend. Download and install it from [here](https://www.docker.com/products/docker-desktop). + +##### VS Code Extensions: + +1. **C# Dev Kit for Visual Studio Code**: While VS Code does support C# development, this specific extension provides advanced features and functionalities tailored for C# development within Azure. It's especially handy for Azure Functions development in C#. Install it from [here](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit). + +1. **Azure Tools**: This extension provides a set of tools for working with Azure services directly from VS Code. It's a must-have for managing Azure resources and services. Install it from [here](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack). + +1. **Docker Extension**: This extension provides a set of tools for working with Docker containers directly from VS Code. It's essential for containerizing and deploying our ASP.NET Core backend. Install it from [here](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker). + +1. **Azure Container Apps Extension**: This extension provides a set of tools for working with Azure Container Apps directly from VS Code. It's essential for deploying our ASP.NET Core backend. Install it from [here](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurecontainerapps). + +1. **Azure Static Web Apps Extension**: This extension provides a set of tools for working with Azure Static Web Apps directly from VS Code. It's essential for deploying our Node.js frontend. Install it from [here](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurestaticwebapps). + +:::info +Checkout the demo bytes for [Intelligent Apps](https://developer.microsoft.com/en-us/reactor/series/S-1308/?wt.mc_id=blog_S-1308_webpage_reactor&ocid=buildia24_60days_blogs) with Azure Container Apps where the product team gives a walkthrough on using open-source vector databases and building a multi-LLM chat application. +::: + +### Creating Azure Resources + +With the prerequisites set, the next step is to create necessary Azure resources. These resources include Azure Communication Services for handling voice calls, Azure AI Language for sentiment analysis, and Azure OpenAI as an alternative for more complex sentiment analysis tasks. Here’s how to set each of these up. + +#### Azure Communication Services Resource + +**Azure Communication Services (ACS)** will manage all aspects of voice calling within our application. To create a new ACS resource: + +1. **Open Azure Portal**: Navigate to the [Azure Portal](https://portal.azure.com) and sign in with your Azure account. +1. **Create Resource**: Click on the "+ Create a resource" button found on the upper-left corner of the Azure Portal dashboard. +1. **Search for Communication Services**: In the "Search the Marketplace" field, enter "Communication Services" and select it from the dropdown list. +1. **Create**: Click the "Create" button. + 1. On the "Basics" tab, fill in the required fields: + + 1. **Subscription**: Choose your Azure subscription. + 1. **Resource group**: Create a new resource group or select an existing one. + 1. **Resource name**: Enter a name for your Communication Services resource. + 1. **Region**: Select a region near you or your target audience. + 1. Click "Review + create" to review your settings and then click the "Create" button to provision the resource. +1. **Configure**: + 1. Once the deployment is complete, navigate to your resource. + 1. Under the "Keys and Endpoint" section, note down your connection string. You'll need this to configure the backend of your application. +1. **Get a UserToken** - For initial frontend testing, we need a user token. In your Azure Communication Services resource, navigate to the "Identities & User Access Tokens" section and create a new user with 'Voice and video calling (VOIP)' services. Note down the user token for use later in the tutorial. + +#### Obtain a Free Trial Phone Number + +To enable PSTN calling capabilities (optional), you'll need a phone number. Azure Communication Services offers a free trial phone number for testing purposes. Here's how to obtain one: + +1. **Navigate to Your ACS Resource**: Sign in to the [Azure Portal](https://portal.azure.com) and go to your newly created Azure Communication Services resource. + +1. **Phone Numbers Section**: On the left-hand menu of your ACS resource overview page, find and select the **Phone Numbers** option. This section allows you to manage phone numbers associated with your ACS resource. + +1. **Get Trial Number**: Inside the Phone Numbers section, you'll see an option to get a free trial phone number. Click on the **Get** button next to the Free Trial Number. Azure will prompt you with the terms and conditions for using a trial number. Please read through these carefully as they contain important information regarding the limitations and permitted use of the free trial number. + +1. **Confirmation**: After accepting the terms, Azure will automatically allocate a trial phone number to your ACS resource. This process may take a few moments. Once completed, the number will be displayed on the screen. Note this number as you will need it for configuring voice calling capabilities in your application. + +* **Note**: The free trial phone number comes with certain limitations. For example, there may be restrictions on the number of calls that can be made or received, call duration, and available features compared to a purchased number. These restrictions are in place to manage the service's use during the trial period. + +* **Note**: If you don't, or can't, obtain a free trial phone number, you can still proceed with the rest of the tutorial. The application will work with VoIP calling capabilities, and you can always add a phone number later if needed. Either clone and run the [Azure Communication Service Calling Quickstart](https://learn.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/getting-started-with-calling?tabs=uwp&pivots=platform-web&ocid=buildia24_60days_blogs) or the [Calling Hero App](https://learn.microsoft.com/azure/communication-services/samples/calling-hero-sample?pivots=platform-web&ocid=buildia24_60days_blogs) and use a user ID or group ID respective when making a call. + +Note down the phone number for use later in the tutorial. + +#### Azure AI Resource + +For basic sentiment analysis, we'll use **Azure AI Language** services. Here's how to set up this resource: + +1. **Create Resource**: From the Azure Portal dashboard, click "+ Create a resource". +1. **Search for AI Language**: Type "Language" in the search bar and select "Language" from the results. +1. **Create**: Press the "Create" button. + 1. Complete the form: + 1. **Subscription** and **Resource group**: As before. + 1. **Name**: Give your AI Language service a unique name. + 1. **Pricing tier**: Select the pricing tier that fits your needs (you can start with the free tier for testing purposes). + 1. **Region**: Choose the same region as your Communication Services to minimize latency. + 1. Proceed to "Review + create" and then click "Create". +1. **Configure**: + 1. After your Language resource is deployed, go to it and note down the Key1 and Endpoint from the "Keys and endpoint" section. + +#### Azure OpenAI Resource + +To leverage more advanced AI capabilities, we also integrate **Azure OpenAI Service**. Setting up this resource involves: + +1. **Create Resource**: Again, from the Azure Portal dashboard, click "+ Create a resource". +1. **Search for OpenAI**: Enter "OpenAI" in the search field and select "OpenAI Service" from the dropdown. +1. **Create**: Hit the "Create" button. + 1. Fill in the details like you did for the Language service, taking special note of the **Subscription**, **Resource group**, **Name**, **Pricing tier**, and **Region**. + 1. Review your settings and click "Create". +1. **Configure**: Before you can generate text or inference, you need to deploy a model. You can select from one of several available models in Azure OpenAI Studio. To deploy a model, follow these steps: + * Sign in to [Azure OpenAI Studio](https://oai.azure.com/) + * Choose the subscription and the Azure OpenAI resource to work with and select Use resource. + * Under Management select Deployments. + * Select Create new deployment and configure the following fields: Model, Deployment name +1. **Keys**: Once deployed, navigate to the OpenAI resource in Azure Portal. - In the "Keys and endpoint" section, copy Key1 and Endpoint. These will be crucial for integrating advanced sentiment analysis into your application. + +### Developing the Node.js Frontend + +In this section, we'll dive into creating the front-end of our real-time voice sentiment analysis application using Node.js. The front end will provide a simple yet functional user interface (UI) for making voice calls and displaying closed captions generated from the conversation. We'll also ensure it can display the sentiment analysis results in real-time. + +#### Setting Up Your Node.js Environment + +First, ensure Node.js is installed on your development machine. If not, download and install it from the [official Node.js website](https://nodejs.org/). With Node.js installed, you can now set up your project environment. + +1. Create a New Directory: Create a new folder for your project. This is where all your Node.js front-end files will reside. + + `bash` + ``` + mkdir voice-sentiment + cd voice-sentiment + ``` + +1. **Initialize Your Project**: Run the following command to create a `package.json` file, which will keep track of your project dependencies. + + `bash` + ``` + npm init -y + ``` + +1. **Install Parcel**: We'll use Parcel, a web application bundler, for a seamless development experience. Install Parcel as a development dependency. + + `bash` + ``` + npm install parcel --save-dev + ``` + +1. **Install Azure Communication Calling SDK**: This SDK is essential for integrating voice calling features in our front-end. + + `bash` + ``` + npm install @azure/communication-calling --save + ``` + +#### Creating the UI for Calling + +Now, let's create the basic structure of our frontend application. This includes setting up HTML, CSS, and JavaScript files. + +1. **HTML**: Create an index.html file in your project root. This file will serve as the entry point for your application. + + `html` + ``` + + + + + + Real-time Voice Sentiment Analysis + + + +
+ + + +
+
+ + + + ``` + +1. **CSS**: Create a `styles.css` file to style your application. Feel free to customize the styles as per your preference. + + `css` + ``` + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f5f5f5; + } + + #app { + text-align: center; + } + + input[type="text"], button { + padding: 10px; + margin: 10px; + border: 1px solid #ccc; + border-radius: 5px; + } + + #captionsArea { + margin-top: 20px; + } + ``` + +1. **JavaScript**: Create an `app.js` file. This is where we'll write the logic for initializing voice calling and handling sentiment analysis. + +Let's start by setting up a basic structure for making and ending calls using the Azure Communication Calling SDK you previously installed. + + `javascript` + ``` + import { CallClient, CallAgent } from '@azure/communication-calling'; + + let callAgent; + let acsPhoneNumber; + let tokenCredential; + let captions; + + const calleeInput = document.getElementById('callee-phone-input'); + const callButton = document.getElementById('call-phone-button'); + const hangUpButton = document.getElementById('hang-up-phone-button'); + + async function initCallAgent() { + const callClient = new CallClient(); + + // Hard code token for now, will be replaced with actual token later + tokenCredential = new AzureCommunicationTokenCredential("<>"); + acsPhoneNumber = "<>"; + + callAgent = await callClient.createCallAgent(tokenCredential); + attachCallListeners(); + } + + function attachCallListeners() { + callButton.addEventListener('click', startCall); + hangUpButton.addEventListener('click', endCall); + } + + async function startCall() { + const callee = calleeInput.value; + if (callee) { + const call = callAgent.startCall([{phoneNumber: callee}]); + // More call handling code here + } + } + + async function endCall() { + if (call) { + await call.hangUp({forEveryone: true}); + // toggle button states + hangUpPhoneButton.disabled = true; + callPhoneButton.disabled = false; + } + } + + initCallAgent(); + ``` + +**Note**: Make sure to replace placeholders like `tokenCredential` with actual values obtained from your backend or Azure Communication Services resource. + +#### Testing and Running Your Application + +1. **Run Your Application**: Use Parcel to serve your application. Run the following command in your project root. + + `bash` + ``` + npx parcel index.html + ``` + * Visit the URL provided by Parcel to view your application. + +1. **Testing**: Test the calling functionality by entering a valid phone number and clicking the "Start Call" button. Ensure you have backend support to handle getting a token for Azure Communication Services. + +#### Handle UserIds and GroupIds + +If you didn't acquire a phone number above, we'll add code here to handle user and group ids. This will allow you to make calls without a phone number. To startCall() add: + +`javascript` +``` +// start a call to phone +const callee = calleePhoneInput.value; + +const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const phoneNumberPattern = /^\+\d+$/; +const userIdPattern = /^8:acs:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[0-9a-f-]+$/i; + +if (guidPattern.test(callee)) { + call = callAgent.join({ groupId: callee}); +} else if (phoneNumberPattern.test(callee)) { + call = callAgent.startCall( + [{phoneNumber: callee}], { alternateCallerId: {phoneNumber: acsPhoneNumber} + }); +} else if (userIdPattern.test(callee)) { + call = callAgent.startCall({ communicationUserId: callee }); +} else { + console.error('Invalid input. Must be a phone number, user ID, or GUID'); + return; +} +``` +#### Adding Captions + +Under `More calling code here` add the following code to check for, and enable, captions: + +`javascript` +``` +captions = call.feature(Features.Captions).captions; +transcript = []; +try { + if (!captions.isCaptionsFeatureActive) { + await captions.startCaptions({ spokenLanguage: 'en-us' }); + } + captions.on('CaptionsReceived', captionsReceivedHandler); +} catch (e) { + console.error('startCaptions failed', e); +} +``` + +Add a `captionsReceivedHandler` function to parse the captions and add it to the caption area: + +`javascript` +``` +function captionsReceivedHandler(captionsReceivedEvent) { + let mri = ''; + + if (captionData.speaker.identifier.kind === 'communicationUser') { + mri = captionData.speaker.identifier.communicationUserId; + mri = mri.slice(-8); + } else if (captionData.speaker.identifier.kind === 'microsoftTeamsUser') { + mri = captionData.speaker.identifier.microsoftTeamsUserId; + mri = mri.slice(-8); + } else if (captionData.speaker.identifier.kind === 'phoneNumber') { + mri = captionData.speaker.identifier.phoneNumber; + } + + let displayName = captionData.speaker.displayName || mri; + + // Get the captions area container + let captionAreasContainer = document.getElementById('captionsArea'); + + // Generate a class name based on the MRI + const newClassName = `prefix${mri.replace(/[:\-+]/g, '')}`; + + // Generate the caption text + const captionText = `${captionData.timestamp.toUTCString()} ${displayName}: ${captionData.captionText ?? captionData.spokenText}`; + + // Try to find an existing caption container + let captionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`); + + // If no existing caption container was found, create a new one + if (!captionContainer) { + captionContainer = document.createElement('div'); + captionContainer.setAttribute('isNotFinal', 'true'); + captionContainer.classList.add(newClassName, 'caption-item'); + captionAreasContainer.appendChild(captionContainer); + } + + // Set the caption text + captionContainer.textContent = captionText; + + // If the caption is final, update the 'isNotFinal' attribute + if (captionData.resultType === 'Final') { + captionContainer.setAttribute('isNotFinal', 'false'); + } +} +``` + +Test the calling functionality by entering a valid phone number (or user ID, or group ID) and clicking the "Start Call" button. + +### Next Steps + +Continue to the [next part](https://azure.github.io/cloud-native/60daysofia/real-time-voice-sentiment-analysis-system-2) of this topic to further explore building, deploying and testing your intelligent app for a real=time voice analysis system. \ No newline at end of file diff --git a/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-2.md b/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-2.md new file mode 100644 index 0000000000..f6eadb465c --- /dev/null +++ b/website/blog-60daysofIA/2024-04-17/real-time-voice-sentiment-analysis-system-2.md @@ -0,0 +1,786 @@ +--- +date: 2024-04-17T09:05 +slug: real-time-voice-sentiment-analysis-system-2 +title: "7.5 Real-time Voice Sentiment Analysis System 2" +authors: [cnteam] +draft: false +hide_table_of_contents: false +toc_min_heading_level: 2 +toc_max_heading_level: 3 +keywords: [Cloud, Data, AI, AI/ML, intelligent apps, cloud-native, 60-days, enterprise apps, digital experiences, app modernization, serverless, ai apps] +image: https://github.com/Azure/Cloud-Native/blob/main/website/static/img/ogImage.png +description: "In today's fast-paced digital world, understanding customer sentiment in real-time during voice calls can provide businesses with a competitive edge. This guide will show developers how to build a robust real-time voice sentiment analysis application using several key Azure services. In this second part, we will develop the backend to handle interactions with Azure Communication Services, Azure AI Language and Azure OpenAI, connect it to the frontend, and deploy to Azure Container Apps ." +tags: [Build-Intelligent-Apps, 60-days-of-IA, learn-live, hack-together, community-buzz, ask-the-expert, azure-kubernetes-service, azure-functions, azure-openai, azure-container-apps, azure-cosmos-db, github-copilot, github-codespaces, github-actions] +--- + + + + + + + + + + + + + + + + + + +## Real-time Voice Sentiment Analysis System Using Azure Communication Services, Azure AI and Azure OpenAI (Part 2) + +In the [first part](https://azure.github.io/Cloud-Native/60daysofIA/real-time-voice-sentiment-analysis-system-1) of this topic, we setup all the Azure resources like the [Azure Communication Services](https://learn.microsoft.com/azure/communication-services/overview?ocid=buildia24_60days_blogs) for VoIP and PSTN, [Azure AI Language](https://learn.microsoft.com/azure/ai-services/language-service/overview?ocid=buildia24_60days_blogs), [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview?ocid=buildia24_60days_blogs), and developed the frontend for a simple yet functional UI to make voice calls and display closed captions generated from the conversation. In this second part, we will develop the backend to handle interactions with Azure Communication Services, Azure AI Language and Azure OpenAI, connect it to the frontend, and deploy to [Azure Container Apps](https://learn.microsoft.com/azure/container-apps/overview?ocid=buildia24_60days_blogs). Let’s get started! + +### Prerequisite + +To follow this tutorial, ensure you have completed the [first part](https://azure.github.io/Cloud-Native/60daysofIA/real-time-voice-sentiment-analysis-system-1) of this topic. + +### Setting Up the Backend with ASP.NET Core + +The backend of our real-time voice sentiment analysis application, built with ASP.NET Core, plays a crucial role. It will handle interactions with Azure Communication Services for voice calling, manage sentiment analysis with Azure AI Language and Azure OpenAI, and serve the necessary tokens and endpoints to our frontend. Let's set this up step by step. + +#### Step 1: Create a New ASP.NET Core Project + +1. **Open Your Command Line**: Navigate to the directory where you want to create your project. + +1. **Create the Project**: Execute the following command to create a new ASP.NET Core Web API project. This command will generate a basic template for a RESTful API application. + + `bash` + ``` + dotnet new webapi --no-https + ``` +**Note**: We are turning off HTTPS for simplicity in this guide. For production environments, HTTPS should be enabled and properly configured. + +#### Step 2: Add Azure SDK Packages + +Our backend needs to interact with Azure Communication Services, Azure AI Language, and Azure OpenAI. We'll add the necessary NuGet packages for these services. + +1. **Azure Communication Services SDK**: This package allows us to manage voice calling and access tokens. + + `bash` + ``` + dotnet add package Azure.Communication.CallingServer + ``` + +1. **Azure AI Text Analytics SDK**: We'll use this for sentiment analysis through Azure AI Language. + + `bash` + ``` + dotnet add package Azure.AI.TextAnalytics + ``` + +1. **Azure OpenAI SDK**: This package is used to interact with Azure OpenAI for more complex sentiment analysis. + + `bash` + ``` + dotnet add package Azure.AI.OpenAI --prerelease + ``` + +#### Step 3: Set Up Configuration + +1. **appsettings.json**: Open or create the `appsettings.json` file in your project root. Add placeholders for the keys and endpoints for the Azure services. Here’s an example configuration: + + `json` + ``` + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AzureSettings": { + "AZURE_LANGUAGE_SERVICE_KEY": "YOUR_AZURE_LANGUAGE_KEY", + "AZURE_LANGUAGE_SERVICE_ENDPOINT": "YOUR_AZURE_OPENAI_ENDPOINT", + "OPENAI_ENDPOINT": "YOUR_AZURE_OPENAI_ENDPOINT", + "OPENAI_KEY": "YOUR_AZURE_OPENAI_KEY", + "OPENAI_DEPLOYMENT_NAME": "YOUR_AZURE_OPENAI_ENDPOINT_DEPLOYMENT", + "COMMUNICATION_CONNECTION_STRING": "YOUR_AZURE_COMMUNICATION_SERVICES_CONNECTION_STRING" + } + } + ``` + * Replace the placeholders with your actual Azure resource keys and endpoints. + +1. **Configure Services in Program.cs**: Read the endpoint from appsettings.json and configure the services in the `Program.cs` file. + + `csharp` + ``` + // Set keys and configuration + var azureSettings = builder.Configuration.GetSection("AzureSettings"); + + string GetSetting(string key) => azureSettings[key] ?? throw new Exception($"{key} is not set"); + + var languageKey = GetSetting("AZURE_LANGUAGE_SERVICE_KEY"); + var languageEndpoint = GetSetting("AZURE_LANGUAGE_SERVICE_ENDPOINT"); + AzureKeyCredential credentials = new AzureKeyCredential(languageKey); + Uri endpoint = new Uri(languageEndpoint); + var textAnalyticsClient = new TextAnalyticsClient(endpoint, credentials); + + var OPENAI_ENDPOINT = GetSetting("OPENAI_ENDPOINT"); + var OPENAI_KEY = GetSetting("OPENAI_KEY"); + var OPENAI_DEPLOYMENT_NAME = GetSetting("OPENAI_DEPLOYMENT_NAME"); + OpenAIClient openAIClient = new OpenAIClient( + new Uri(OPENAI_ENDPOINT), + new AzureKeyCredential(OPENAI_KEY)); + + var communicationConnectionString = GetSetting("COMMUNICATION_CONNECTION_STRING"); + var communicationClient = new CommunicationIdentityClient(communicationConnectionString); + ``` + +1. **Implement the Token Endpoint**: + +In your ASP.NET Core project Program.cs, create a new endpoint to generate and return an access token. We'll call this endpoint from our client later. + +`csharp` +``` +app.MapGet("api/token", () => +{ + var identityAndTokenResponse = communicationClient.CreateUserAndToken(scopes: [CommunicationTokenScope.VoIP]); + + + var result = new TokenResponse() + { + Token = identityAndTokenResponse.Value.AccessToken.Token, + PhoneNumber = "+12533192954", + }; + + return Results.Json(result); +}) +.WithName("Token") +.WithOpenApi(); +``` + +We'll also need a TokenResponse object to return the token and phone number: + +`csharp` +``` +public class TokenResponse +{ + public string Token { get; set; } + public string PhoneNumber { get; set; } +} +``` + +#### Step 4: Implement Sentiment Analysis Endpoints + +With the services configured, you can now implement the API endpoints needed for your application. Here are the essentials: + +1. **Token Provisioning**: An endpoint to provide access tokens for Azure Communication Services. + +1. **Sentiment Analysis**: Endpoints to analyze sentiment using either Azure AI Language or Azure OpenAI. + +`csharp` +``` +app.MapPost("api/sentiment", (SentimentRequest request) => +{ + var result = SentimentAnalysisWithAzureLanguage(request.Content); + + + return Results.Json(result); +}) +.WithName("Sentiment") +.WithOpenApi(); +``` + +This endpoint takes a SentimentRequest object and returns a SentimentResponse object and calls one of our sentiment analysis methods, which we'll implement shortly + +`csharp` +``` +public class SentimentRequest +{ + public string Analyzer { get; set; } = string.Empty; + public string ParticipantToAnalyze { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; +} + +public class SentimentResult +{ + public string Sentiment { get; set; } = string.Empty; + public double PositiveContentScore { get; set; } +} +``` + +We'll create a method that uses Azure AI Language and another that uses Azure OpenAI. Here's an example of how to implement sentiment analysis using Azure AI Language: + +`csharp` +``` +SentimentResult SentimentAnalysisWithAzureLanguage(string document) +{ + var review = textAnalyticsClient.AnalyzeSentiment(document); + return new SentimentResult() + { + Sentiment = review.Value.Sentiment.ToString(), + PositiveContentScore = review.Value.ConfidenceScores.Positive + }; +} +``` + +And we'll create method endpoint that uses Azure OpenAI: + +`csharp` +``` +SentimentResult SentimentAnalysisWithGPT(string document) +{ + var chatCompletionsOptions = new ChatCompletionsOptions() + { + DeploymentName = "shawn-deployment", + Messages = + { + new ChatRequestSystemMessage(SentimentAnalysisPrompt), + new ChatRequestUserMessage(document), + }, + Temperature = (float)1, + MaxTokens = 800 + }; + + Response response = openAIClient.GetChatCompletions(chatCompletionsOptions); + + return new SentimentResult() { + Sentiment = response.Value.Choices[0].Message.Content + }; +} +``` + +#### Step 5: Test Your Backend + +Before moving forward, make sure to test your backend. Use tools like Postman or Swagger UI to ensure your endpoints are responsive and returning the expected results. + +Your final backend should look like this: + +`csharp` +``` +using Azure; +using Azure.AI.TextAnalytics; +using Azure.AI.OpenAI; +using Azure.Communication.Identity; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Set keys and configuration +var azureSettings = builder.Configuration.GetSection("AzureSettings"); + +string GetSetting(string key) => azureSettings[key] ?? throw new Exception($"{key} is not set"); + +var languageKey = GetSetting("AZURE_LANGUAGE_SERVICE_KEY"); +var languageEndpoint = GetSetting("AZURE_LANGUAGE_SERVICE_ENDPOINT"); +AzureKeyCredential credentials = new AzureKeyCredential(languageKey); +Uri endpoint = new Uri(languageEndpoint); +var textAnalyticsClient = new TextAnalyticsClient(endpoint, credentials); + +var OPENAI_ENDPOINT = GetSetting("OPENAI_ENDPOINT"); +var OPENAI_KEY = GetSetting("OPENAI_KEY"); +var OPENAI_DEPLOYMENT_NAME = GetSetting("OPENAI_DEPLOYMENT_NAME"); +OpenAIClient openAIClient = new OpenAIClient( + new Uri(OPENAI_ENDPOINT), + new AzureKeyCredential(OPENAI_KEY)); + +var communicationConnectionString = GetSetting("COMMUNICATION_CONNECTION_STRING"); +var communicationClient = new CommunicationIdentityClient(communicationConnectionString); + +const string SentimentAnalysisPrompt = "Please analyze the sentiment of the following text. The sentiment can be positive, negative, or neutral. Response on with one of those three values"; + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapGet("api/token", () => +{ + var identityAndTokenResponse = communicationClient.CreateUserAndToken(scopes: [CommunicationTokenScope.VoIP]); +  + + var result = new TokenResponse() + { + Token = identityAndTokenResponse.Value.AccessToken.Token, + PhoneNumber = "+12533192954", + }; + + return Results.Json(result); +}) +.WithName("Token") +.WithOpenApi(); + +app.MapPost("api/sentiment", (SentimentRequest request) => +{ + var result = SentimentAnalysisWithAzureLanguage(request.Content); +  + + return Results.Json(result); +}) +.WithName("Sentiment") +.WithOpenApi(); + +  +app.Run(); + + +SentimentResult SentimentAnalysisWithAzureLanguage(string document) +{ + var review = textAnalyticsClient.AnalyzeSentiment(document); + return new SentimentResult() + { + Sentiment = review.Value.Sentiment.ToString(), + PositiveContentScore = review.Value.ConfidenceScores.Positive + }; +} + +// // Function to get a response from OpenAI's ChatGPT +SentimentResult SentimentAnalysisWithGPT(string document) +{ + var chatCompletionsOptions = new ChatCompletionsOptions() + { + DeploymentName = "shawn-deployment", + Messages = + { + new ChatRequestSystemMessage(SentimentAnalysisPrompt), + new ChatRequestUserMessage(document), + }, + Temperature = (float)1, + MaxTokens = 800 + }; + + Response response = openAIClient.GetChatCompletions(chatCompletionsOptions); + + return new SentimentResult() { + Sentiment = response.Value.Choices[0].Message.Content + }; +} +public class TokenResponse +{ + public string Token { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; +} + +public class SentimentRequest +{ + public string Analyzer { get; set; } = string.Empty; + public string ParticipantToAnalyze { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; +} + +public class SentimentResult +{ + public string Sentiment { get; set; } = string.Empty; + public double PositiveContentScore { get; set; } +} +``` + +:::info +Earn Microsoft-verified credentials for cloud-native app development skills by passing the [Azure Container Apps](https://learn.microsoft.com/credentials/applied-skills/deploy-cloud-native-apps-using-azure-container-apps/?ocid=buildia24_60days_blogs) assessment to elevate your professional profile. +::: + +### Connecting the Frontend with the ASP.NET Core Backend + +After setting up the backend and the frontend of our real-time voice sentiment analysis application separately, it’s time to connect them. This connection ensures that our Node.js frontend can communicate with the ASP.NET Core backend to do things like obtaining access tokens for Azure Communication Services and conducting sentiment analysis through Azure AI Language and Azure OpenAI. + +#### Step 1: Get the Access Token + +For our application to establish a call, the frontend needs to obtain an access token from Azure Communication Services. In the `app.js` file within your Node.js project, modify the `initCallAgent` function to fetch the token from the backend before initializing the call agent. Use the `fetch` API for this purpose: + +`javascript` +``` +async function initCallAgent() { + try { + const response = await fetch('/api/communications/token'); + const { token, userId } = await response.json(); + + const tokenCredential = new AzureCommunicationTokenCredential(token); + const callClient = new CallClient(); + callAgent = await callClient.createCallAgent(tokenCredential, { displayName: 'Caller' }); + + attachCallListeners(); + } catch (error) { + console.error('Failed to obtain token:', error); + } +} +``` + +#### Step 2: Implement Sentiment Analysis on Captions + +Our application should analyze the sentiment of the transcribed call captions. We’ll capture these captions on the frontend, then send them to our backend for sentiment analysis. + +1. **Make Sentiment Analysis Request:** + +In the `captionsReceivedHandler`, make a request to your backend sentiment analysis endpoint whenever captions are received. Update your captions event handler, under `if (captionData.resultType === 'Final')` to include this: + +`javascript` +``` +if (captionData.resultType === 'Final') { + captionContainer.setAttribute('isNotFinal', 'false'); + transcript.push(captionText); + + // Call the sentiment service + fetch('/api/sentiment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ content: transcript.join('\n') }) + }) + .then(response => response.json()) + .then(data => { + // Handle the sentiment response + const sentimentText = `Sentiment:${data.sentiment} (${data.positiveContentScore})`; + document.getElementById('sentimentScore').textContent = captionText; + + //update sentiment UI + updateSentimentMeter(data.positiveContentScore); + + }); +} +``` + +#### Step 3: Display Analysis Results + +Finally, utilize the sentiment analysis data received from the backend to inform the user about the call’s sentiment in real-time. + +1. **Update the UI Dynamically**: + +Enhance your frontend to dynamically display sentiment analysis results as they are received. We'll add an indicator for positive, neutral, or negative sentiment. + +`javascript` +``` +function updateSentimentMeter(score) { + // Ensure score is between 0 and 1 + score = Math.max(0, Math.min(score, 1)); + + // Convert score to angle: -90° for 0, 90° for 1 + var angle = score * 180 - 90; + + // Rotate the arrow to the corresponding angle + document.getElementById('meterArrow').style.transform = 'rotate(' + angle + 'deg)'; +} +``` + +Create new elements in the HTML to display the sentiment score and meter: + +`html` +``` +
+

Sentiment Score

+
+
+
+``` + +Your complete code for app.js should look like this: + +`javascript` +``` +import { CallClient, CallAgent, Features } from "@azure/communication-calling"; +import { AzureCommunicationTokenCredential } from '@azure/communication-common'; + +let call; +let callAgent; + +const calleePhoneInput = document.getElementById("callee-phone-input"); +const callPhoneButton = document.getElementById("call-phone-button"); +const hangUpPhoneButton = document.getElementById("hang-up-phone-button"); + +let acsPhoneNumber; +let tokenCredential; + +let captions; + +async function init() { + + const response = await fetch('/api/token'); + const { token, phoneNumber } = await response.json(); + + tokenCredential = new AzureCommunicationTokenCredential(token); + acsPhoneNumber = phoneNumber; + + const callClient = new CallClient(); + callAgent = await callClient.createCallAgent(tokenCredential); +} + +init(); + +callPhoneButton.addEventListener("click", () => { + // start a call to phone + const endpointToCall = calleePhoneInput.value; + + const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const phoneNumberPattern = /^\+\d+$/; + const userIdPattern = /^8:acs:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[0-9a-f-]+$/i; + + if (guidPattern.test(endpointToCall)) { + call = callAgent.join({ groupId: endpointToCall}); + } else if (phoneNumberPattern.test(endpointToCall)) { + call = callAgent.startCall( + [{phoneNumber: endpointToCall}], { alternateCallerId: {phoneNumber: acsPhoneNumber} + }); + } else if (userIdPattern.test(endpointToCall)) { + call = callAgent.startCall({ communicationUserId: endpointToCall }); + } else { + console.error('Invalid input. Must be a phone number, user ID, or GUID'); + return; + } +  + + call.on('stateChanged', async () => { + console.log(`Call state: ${call.state}`); + if(call.state === 'Connected') { + console.log('Call connected'); + + + captions = call.feature(Features.Captions).captions; + transcript = []; + try { + if (!captions.isCaptionsFeatureActive) { + await captions.startCaptions({ spokenLanguage: 'en-us' }); + } + captions.on('CaptionsReceived', captionsReceivedHandler); + } catch (e) { + console.error('startCaptions failed', e); + } + } + }); + + hangUpPhoneButton.disabled = false; + callPhoneButton.disabled = true; + }); + + hangUpPhoneButton.addEventListener("click", () => { + // end the current call + call.hangUp({ + forEveryone: true + }); + + // toggle button states + hangUpPhoneButton.disabled = true; + callPhoneButton.disabled = false; + }); + + const captionsReceivedHandler = (captionData) => { + let mri = ''; + + if (captionData.speaker.identifier.kind === 'communicationUser') { + mri = captionData.speaker.identifier.communicationUserId; + mri = mri.slice(-8); + } else if (captionData.speaker.identifier.kind === 'microsoftTeamsUser') { + mri = captionData.speaker.identifier.microsoftTeamsUserId; + mri = mri.slice(-8); + } else if (captionData.speaker.identifier.kind === 'phoneNumber') { + mri = captionData.speaker.identifier.phoneNumber; + } + + let displayName = captionData.speaker.displayName || mri; + + // Get the captions area container + let captionAreasContainer = document.getElementById('captionsArea'); + + // Generate a class name based on the MRI + const newClassName = `prefix${mri.replace(/[:\-+]/g, '')}`; + + // Generate the caption text + const captionText = `${captionData.timestamp.toUTCString()} ${displayName}: ${captionData.captionText ?? captionData.spokenText}`; + + // Try to find an existing caption container + let captionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`); + + // If no existing caption container was found, create a new one + if (!captionContainer) { + captionContainer = document.createElement('div'); + captionContainer.setAttribute('isNotFinal', 'true'); + captionContainer.classList.add(newClassName, 'caption-item'); + captionAreasContainer.appendChild(captionContainer); + } + + // Set the caption text + captionContainer.textContent = captionText; + + // If the caption is final, update the 'isNotFinal' attribute + if (captionData.resultType === 'Final') { + captionContainer.setAttribute('isNotFinal', 'false'); + transcript.push(captionText); + + + // Call the sentiment service + fetch('/api/sentiment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ content: transcript.join('\n') }) + }) + .then(response => response.json()) + .then(data => { + // Handle the sentiment response + const sentimentText = `Sentiment:${data.sentiment} (${data.positiveContentScore})`; + document.getElementById('sentimentScore').textContent = captionText; + updateSentimentMeter(data.positiveContentScore); + }); + } + + function updateSentimentMeter(score) { + // Ensure score is between 0 and 1 + score = Math.max(0, Math.min(score, 1)); + + // Convert score to angle: -90° for 0, 90° for 1 + var angle = score * 180 - 90; + + // Rotate the arrow to the corresponding angle + document.getElementById('meterArrow').style.transform = 'rotate(' + angle + 'deg)'; + } +}; +``` + +... and the HTML should look like this + +`HTML` +``` + + + + + + Azure Communication Service - Realtime Sentiment Analysis + + + +

Azure Communication Services

+ +
+ +   + +
+

Sentiment Score

+
+
+
+
+ +
+ + + +``` + +#### Testing the Complete System + +With the frontend and backend now connected, thoroughly test your application: + +* Ensure the voice call setup and teardown works flawlessly. +* Verify that captions are accurately captured and displayed. +* Confirm that sentiment analysis requests are successfully handled and that the results make sense for the given captions. + +:::info +Explore a variety of Azure Container Apps [code samples](https://learn.microsoft.com/samples/browse/?terms=azure%20container%20apps&ocid=buildia24_60days_blogs) for a quick start to your intelligent app development with cloud-native technologies. +::: + +### Deployment + +With the real-time voice sentiment analysis system fully developed, including both the frontend and backend components, the next critical step is deployment. We'll deploy the ASP.NET Core backend using Azure Container Apps and the Node.js frontend through Azure Static Web Apps. + +#### Step 1: Prepare the Backend for Deployment + +##### Create a Dockerfile + +1. **Navigate to Your Backend Project Directory**: Open the terminal or command prompt and ensure you're in the root directory of your ASP.NET Core project. + +1. **Create a Dockerfile**: In the root of your project, create a file named `Dockerfile` with no file extension. This file will contain instructions for building a Docker image for your application. + +1. **Define the Dockerfile Contents**: Open the `Dockerfile` in your editor and add the following content: + +`Dockerfile` +``` +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["VoiceSentimentBackend.csproj", "."] +RUN dotnet restore "./VoiceSentimentBackend.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "VoiceSentimentBackend.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "VoiceSentimentBackend.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "VoiceSentimentBackend.dll"] +``` + +**Note**: Adjust `"VoiceSentimentBackend.csproj"` to match your project's name. + +##### Create and Test the Docker Image Locally + +1. **Build the Docker Image**: Run the following command in your terminal: + + `bash` + ``` + docker build -t voicesentimentbackend . + ``` + +1. **Run Your Docker Container**: To test the Docker container locally, execute: + + `bash` + ``` + docker run -d -p 8080:80 --name myapp voicesentimentbackend + ``` + +1. **Verify**: Open your browser and navigate to `http://localhost:8080/api/token` to ensure your application is running correctly in the container. + +#### Step 2: Deploy the Backend to Azure Container Apps + +Azure Container Apps offers a fully managed serverless container service. It's an excellent choice for deploying containers without managing complex infrastructure. + +1. **Create a Container App Environment**: Use VS Code to create an Azure Container App and deploy your backend. Follow the [official documentation](https://learn.microsoft.com/azure/container-apps/deploy-visual-studio-code?ocid=buildia24_60days_blogs) for detailed instructions. Type Crtl+Shift+P to open the command palette and type "Azure Container Apps: Create New Container App" and follow the prompts. + +1. **Verify Deployment**: Use the FQDN obtained from the previous command to verify your application is accessible via the internet. + +#### Step 3: Deploy the Frontend to Azure Static Web Apps + +Azure Static Web Apps is a service tailored for static web applications, providing global distribution, serverless APIs, and seamless integration with GitHub for continuous deployment. + +##### Setting Up Continuous Deployment with GitHub + +1. **Push Your Frontend Code to GitHub**: Ensure your Node.js frontend code is in a GitHub repository. + +1. **Create a Static Web App Resource**: In the Azure Portal, create a new Static Web App resource and connect it to your GitHub repository. Specify your build details during the setup. + +1. **Verify Deployment**: Once the GitHub Actions workflow is completed, your Static Web App will be accessible via the provided URL. Check it to ensure your frontend is live and functional. + +#### Conclusion + +Over the course of this guide, you've: + +* Set up and configured critical Azure resources, including Azure Communication Services for managing voice calls, Azure AI Language for basic sentiment analysis, and Azure OpenAI for more nuanced sentiment insights. +* Developed a Node.js frontend to interface with users, initiating and managing voice calls, and dynamically displaying sentiment analysis results. +* Implemented an ASP.NET Core backend to handle business logic, interact with Azure services, and provide APIs for the frontend. +* Connected your frontend and backend, ensuring seamless communication and data flow within your application. +* Deployed your application to Azure, leveraging Azure Container Apps for the backend and Azure Static Web Apps for the frontend, making your project accessible from anywhere. + +#### Future Enhancements + +While your current application is fully functional, there's always room for improvement and expansion. Consider the following possibilities for future development: + +* **Language Support**: Expand the application to support additional languages, enhancing its accessibility and utility across different geographical locations. +* **Advanced AI Insights**: Explore deeper insights beyond sentiment, such as emotional tone, intent recognition, or specific topic detection, to provide more detailed analysis of voice calls. \ No newline at end of file diff --git a/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-1.png b/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-1.png new file mode 100644 index 0000000000..4e7c214318 Binary files /dev/null and b/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-1.png differ diff --git a/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-2.png b/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-2.png new file mode 100644 index 0000000000..7076ded30f Binary files /dev/null and b/website/static/img/60-days-of-ia/blogs/2024-04-17/7-4-2.png differ