Showing posts with label Simple. Show all posts
Showing posts with label Simple. Show all posts

Wednesday, February 25, 2026

Explore A2A protocol with .NET and GitHub Models

Let's explore the Agent-to-Agent (A2A) protocol using .NET. The A2A protocol standardizes communication between agents. It allows agents built with different frameworks and technologies to seamlesssly communicate with one-another.

What's A2A?

A2A is a standardized protocol that supports:

  • Agent discovery through agent cards
  • Message-based communication between agents
  • Long-running agentic processes via tasks
  • Cross-platform interoperability between different agent frameworks

The A2A protocol was developed by Google and later donated to the Linux Foundation.For more information, visit A2A protocol specification.

Source Code: https://github.com/medhatelmasry/A2Aapi

Get Started

In the following example, we will learn how to expose an agent with A2A. The example uses an AI model hosted on GitHub. In addition, we will use Swagger to simplify testing.

In a working directory on your computer, create an ASP.NET Minimal API project named A2Aapi with the following terminal window command:

dotnet new webapi -o A2Aapi
cd A2Aapi
dotnet new gitignore

Install the following NuGet packages:

# Hosting.A2A.AspNetCore for A2A protocol integration
dotnet add package Microsoft.Agents.AI.Hosting.A2A.AspNetCore -v 1.0.0-preview.260219.1

# Libraries to connect to GitHub AI models
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.AI.OpenAI

# Swagger to test app
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Swashbuckle.AspNetCore


Configure connection to GitHub AI Models

You will need to get a Personal Access Token from GitHub. If this is the first time, follow this tutorial.

Add the following JSON to appsettings.Development.json file:

"GitHub": {
    "Token": "put-your-github-personal-access-token-here",
    "ApiEndpoint": "https://models.github.ai/inference",
    "Model": "openai/gpt-4o-mini"
}

NOTE: Replace put-your-github-personal-access-token-here with your GitHub Personal Access Token.

Edit the .gitignore file in the A2Aapi folder and add to it appsettings.Development.json so that your secrets do not find their way into source control by mistake.

Replace contents of Program.cs with the following code:

using OpenAI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.AI;
using Azure;
using OpenAI.Chat;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddSwaggerGen();

string githubToken = builder.Configuration["GitHub:Token"]
    ?? throw new InvalidOperationException("GitHub:Token is not set.");
string apiEndpoint = builder.Configuration["GitHub:ApiEndpoint"]
    ?? throw new InvalidOperationException("GitHub:ApiEndpoint is not set.");
string model = builder.Configuration["GitHub:Model"]
    ?? throw new InvalidOperationException("GitHub:Model is not set.");

// Register the chat client
IChatClient chatClient = new ChatClient(
    model,
    new AzureKeyCredential(githubToken),
    new OpenAIClientOptions
    {
        Endpoint = new Uri(apiEndpoint)
    }
)
.AsIChatClient();

builder.Services.AddSingleton(chatClient);

// Register agents
var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate.");

var app = builder.Build();

app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();

// Expose the agent via A2A protocol. You can also customize the agentCard
app.MapA2A(pirateAgent, path: "/a2a/pirate", agentCard: new()
{
    Name = "Pirate Agent",
    Description = "An agent that speaks like a pirate.",
    Version = "1.0"
});

app.Run();


Test Agent

Run the web app with:

dotnet run

We have two options to test our agent: we can either use Swagger by pointing our browser to the /swagger endpoint, or we can use the A2Aapi.http REST Client that is built into the ASP.NET Minimal API template.

Option 1 - using Swagger

Point your browser to the URL displayed the the terminal window with /swagger. In my case it would be http://localhost:5112/swagger. You will see an interface similar to this:

Cloose the POST /a2a/pirate/v1/message:stream endpoint.

Click on the "Try it out" 

Enter the following JSON request then click on the Execute button:
{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "Hey pirate! Tell me where have you been",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": "foo"
  }
}

The server response looks like this:

This is the prompt we sent to the agent:

Hey pirate! Tell me where have you been

This is the response from the agent:

Ahoy, matey! I've been sailin' the seven seas, searchin' fer treasure and chasin' down the fiercest storms!

From the shores of Tortuga to the depths of Davy Jones' locker, me heart be filled with tales of adventure. And where be ye anchorin" yer ship, eh?

The response includes the contextId (conversation identifier), messageId (message identifier), and the actual content from the pirate agent.

Option 2 - using .http REST Client

If you are using VS Code, install the following VS Code extension:


Edit the A2Aapi.http in your project and add this request:
###
# Send A2A request to the pirate agent
POST {{A2Aapi_HostAddress}}/a2a/pirate/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "Hey pirate! Tell me where have you been",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": "foo"
  }
}

Click on the "Send Request" link as shown below:

The response will show in a separate panel like this:

AgentCard Configuration

The AgentCard provides metadata about your agent for discovery and integration:

app.MapA2A(agent, "/a2a/my-agent", agentCard: new() {
   Name = "My Agent",
   Description = "A helpful agent that assists with tasks.",
   Version = "1.0",
});

The agent card can be accessed by sending this request:

# Send A2A request to the pirate agent
GET {{baseAddress}}/a2a/pirate/v1/card


Properties of the Agent Card

NameDisplay name of the agent
DescriptionBrief description of the agent
VersionVersion string for the agent
UrlEndpoint URL (automatically assigned if not specified)
CapabilitiesOptional metadata about streaming, push notifications, and other features


Exposing More Agents

You can expose multiple agents in a single application, as long as their endpoints don't collide. Here's an example:

Add the following code to Program.cs right under the "// Register agents" comment line:

var mathAgent = builder.AddAIAgent("math", instructions: "You are a math expert.");
var scienceAgent = builder.AddAIAgent("science", instructions: "You are a science expert.");

Similarly, add these endpoint mappings to Program.cs right above the last "app.Run();" statement:

app.MapA2A(mathAgent, "/a2a/math");
app.MapA2A(scienceAgent, "/a2a/science");

You can test the math agent and science agents with these respective requests:

Test math agent

###
# Send A2A request to the math agent
POST {{A2Aapi_HostAddress}}/a2a/math/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "add 2 and 7",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": null
  }
}


Test science agent

###
# Send A2A request to the science agent
POST {{A2Aapi_HostAddress}}/a2a/science/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "how far is saturn from earth?",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": null
  }
}


Conclusion

Therea re many emerging protocols that are giving us an insight into the future landscapte of the Agentic AI world o the future. This is one amone others. I trust that is article gives you in insight into the significance of the A2A protocol.

References

A2A Integration

Agent2Agent (A2A) Protocol

Thursday, February 12, 2026

docker-compose with SQL Server and ASP.NET

This article discussed one approach to having your ASP.NET development environment work with SQL Server (MSSQL) running in a docker container.

Source code: https://github.com/medhatelmasry/AspMSSQL

It is assumed that the following installed on your computer:

  1. .NET 10.0 
  2. Docker Desktop 
  3. ‘dotnet-ef’ tool 

Setting up SQL Server docker container

To download a suitable SQL Server image from Docker Hub and run it on your local computer, type the following command from within a terminal window:

docker run --cap-add SYS_PTRACE -e ACCEPT_EULA=1 -e MSSQL_SA_PASSWORD=SqlPassword! -p 1444:1433 --name mssql -d mcr.microsoft.com/mssql/server:2022-latest

This starts a container named mssql that listens on port 1444 on your local computer. The sa password is SqlPassword!.

To ensure that the SQL Server container is running, type the following from within a terminal window:

docker ps

You will see a message like the following:

CONTAINER ID   IMAGE                                        ...... NAMES
e84053717017   mcr.microsoft.com/mssql/server:2022-latest   ...... mssql

Creating our ASP.NET MVC App

Create an ASP.NET MVC app named AspMSSQL with SQL Server support by running the following terminal window commands:

dotnet new mvc --auth individual --use-local-db -o AspMSSQL
cd AspMSSQL

To run the web application and see what it looks like, enter the following command:

dotnet watch

The app starts in your default browser and looks like this:

The default page when starting an ASP.NET MVC application.

Let us configure our web application so that the connection string can be constructed from environment variables. Open the Program.cs file in your favourite editor and comment out (or delete) the following statements:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

Replace the above code with the following:

var host = builder.Configuration["DBHOST"] ?? "localhost";
var port = builder.Configuration["DBPORT"] ?? "1444";
var password = builder.Configuration["DBPASSWORD"] ?? "SqlPassword!";
var db = builder.Configuration["DBNAME"] ?? "mydb";
var user = builder.Configuration["DBUSER"] ?? "sa";

string connectionString = $"Server={host},{port};Database={db};UID={user};PWD={password};TrustServerCertificate=True;";

Five environment variables are used in the database connection string. These are: DBHOST, DBPORT , DBPASSWORD, DBNAME and DBUSER. If these environment variables are not found then they will take on default values: localhost, 1444, SqlPassword!, mydb and sa respectively.

Go ahead and delete the connection string from appsettings.json as it is not needed anymore:

"ConnectionStrings": {
  "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-AspMSSQL; MultipleActiveResultSets=true"
},

Entity Framework Migrations

We can instruct our application to automatically process any outstanding Entity Framework migrations. This is done by adding the following statement to Program.cs right before the last app.Run() statement:

using (var scope = app.Services.CreateScope()) {
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<ApplicationDbContext>();    
    context.Database.Migrate();
}

Test app

Now, let's test our web app and see whether it can talk to the containerized MSSQL database server. Run the web application with the following terminal command:

dotnet watch

Click on the Register link on the top right side.

The ASP.NET MVC user register page.

I entered an Email, Password and Confirm password, then clicked on the Register button. The website then displays the following page that requires that you confirm the email address:

User Register Confirmation Page.

Click on the “Click here to confirm your account” link. This leads you to a confirmation page:

After a user confirms email, the user confirm email aler displays.

Login with the email address and password that you registered with.

The message on the top right side confirms that the user was saved and that communication between the ASP.NET MVC app and SQL Server is working as expected.

Dockeri-zing app

We will generate the release version of the application by executing the following command from a terminal window in the root directory of the web app:

dotnet publish -o distrib

The above command instructs dotnet to produce the release version of the application in the distrib directory. When you inspect the distrib directory, you will see files like the following:

A screen capture of the files in the bin folder containing the main DLL named AspMSSQL.dll

The highlighted file in the above image is the main DLL file that is the entry point into the web application. Let us run the DLL. To do this, change to the distrib directory, then run your main DLL file with:

cd distrib
dotnet AspMSSQL.dll

This displays the familiar messages from the web server that the app is ready to be accessed from a browser. 

Screen capture showing the terminal window after executing command "dotnet AspMSSQL.dll"

Hit CTRL C to stop the web server.

We now have a good idea about the ASP.NET artifacts that need to be copied into a container.

In a terminal window, stop and remove the MSSQL container with:

docker rm -f mssql

Return to the root directory of your project by typing the following in a terminal window:

cd ..

Docker image for web app

We need to create a docker image that will contain the .NET runtime. At the time of writing this article, the current version of .NET is 10.0.

We can exclude files from being copied into the container imag Add a file named .dockerignore in the root of the web application with this content:

**/.git
**/.gitignore
**/node_modules
**/npm-debug.log
**/.DS_Store
**/bin
**/obj
**/.vs
**/.vscode
**/.env
**/*.user
**/*.suo
**/.idea
**/coverage
**/.nyc_output
**/docker-compose*.yml
**/Dockerfile*
**/.github
**/README.md
**/LICENSE

Create a text file named Dockerfile and add to it the following content:

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy project file and restore dependencies
COPY ["AspMSSQL.csproj", "."]
RUN dotnet restore "AspMSSQL.csproj"

# Copy the rest of the source code
COPY . .

# Build the application
RUN dotnet build "AspMSSQL.csproj" -c Release -o /app/build

# Publish stage
FROM build AS publish
RUN dotnet publish "AspMSSQL.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app

# Install curl for health checks (optional)
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# Copy published application from publish stage
COPY --from=publish /app/publish .

# Expose port 8080 (HTTP)
EXPOSE 8080

# Set environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production

# Run the application
ENTRYPOINT ["dotnet", "AspMSSQL.dll"]

docker-compose.yml

We will next create a docker yml file that orchestrates the entire system involving two containers: a MSSQL database server and our web app. In the root folder of your application, create a text file named docker-compose.yml and add to it the following content:

services:
  # SQL Server Service
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: aspmsql-mssql
    environment:
      ACCEPT_EULA: 'Y'
      MSSQL_SA_PASSWORD: 'SqlPassword!123'
      MSSQL_PID: 'Developer'
    ports:
      - "1433:1433"
    volumes:
      - ./mssql-data:/var/opt/mssql/data

  # ASP.NET Application Service
  aspmsql-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: aspmsql-app
    depends_on:
      - mssql
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      ASPNETCORE_URLS: http://+:8080
      DBHOST: mssql
      DBPORT: 1433
      DBUSER: sa
      DBPASSWORD: SqlPassword!123
      DBNAME: AspMSSQLDb
    ports:
      - "8080:8080"
    restart: unless-stopped

volumes:
  sqlserver-data:
    driver: local

Running the yml file

To find out if this all works, go to a terminal window and run the following command:

docker-compose up -d --build

Point your browser to http://localhost:8080/ and you should see the main web page. Register a user, confirm the email, and login. It should all work as expected.

Screen capture showing that qq@qq.qq is logged into the web app.

Cleanup

Run the following command to shutdown docker-compose and cleanup:

docker-compose down

Conclusion

We have seen how straight forward and easy it is to containerize an application and its database with docker-compose.

Sunday, October 19, 2025

Small Language Models with AI Toolkit Extension in VS Code

In this article, we will see how we can work with small language models (SLM) from the AI Toolkit extension in VS Code. Though the toolkit can do other things, our focus is to consume an ONNX SLM hosted on Visual Studio Code from a C# application. We will first look at an example that is based on OpenAI packages. We will later use a similar example based on the Sematic Kernal approach.

Companion Video: https://youtu.be/V_eWAM2fxJg

Prerequisites

You will need:

  • The latest version of VS Code
  • .NET version 9.0 or higher

What are small language models (SLMs)?

Small Language Models (SLMs) are compact versions of large language models (LLMs), designed to deliver strong performance in natural language tasks while using significantly fewer computational resources.

What is the AI Toolkit Extension in VS Code?

The AI Toolkit Extension for Visual Studio Code is a powerful, all-in-one environment for building, testing, and deploying generative AI applications—especially useful for developers working with small language models (SLMs).

Getting Started

Install the following Visual Studio Code extension:


Click on the three dots (...) in the left navigation of VS Code, and choose "AI Toolkit".

Click on "Model Catalog".

Scroll down down the list until you find “Local Models” >> ONNX >> Minstral 7B – (CPU – Small, Standard) >> + Add Model.

Once the model is fully downloaded, it will appear under Models >> ONNX.

Right-click on the model and select “Copy Model Name”.

I copied the following name for the "Minstral 7B" model: 

mistral-7b-v02-int4-cpu

Using OpenAI packages

Create a C# console application named AIToolkitOpenAI and add to it required packages with the following terminal window commands:

dotnet new console -n AIToolkitOpenAI
cd AIToolkitOpenAI
dotnet add package OpenAI

Start VS Code with:

code .

Click on the "AI Toolkit" tab in VS Code and make sure that the "Minstral 7B" model is running.

Replace content of Program.cs with this code:

using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text;

var model = "mistral-7b-v02-int4-cpu";
var baseUrl = "http://localhost:5272/v1/"; // root URL for local OpenAI-like server
var apikey = "unused";

OpenAIClientOptions options = new OpenAIClientOptions();
options.Endpoint = new Uri(baseUrl);
ApiKeyCredential credential = new ApiKeyCredential(apikey);
ChatClient client = new OpenAIClient(credential, options).GetChatClient(model);

// Build the prompt
StringBuilder prompt = new StringBuilder();
prompt.AppendLine("You will analyze the sentiment of the following product reviews.");
prompt.AppendLine("Each line is its own review. Output the sentiment of each review in");
prompt.AppendLine("a bulleted list and then provide a general sentiment of all reviews.");
prompt.AppendLine();
prompt.AppendLine("I bought this product and it's amazing. I love it!");
prompt.AppendLine("This product is terrible. I hate it.");
prompt.AppendLine("I'm not sure about this product. It's okay.");
prompt.AppendLine("I found this product based on the other reviews. It worked");

// send the prompt to the model and wait for the text completion
var response = await client.CompleteChatAsync(prompt.ToString());
// display the response
Console.WriteLine(response.Value.Content[0].Text);

Run the application with:

dotnet run

The application does sentiment analysis on what customers think of the product.

This is a sample of the output:

* I bought this product and it's amazing. I love it!: Positive sentiment
* This product is terrible. I hate it.: Negative sentiment
* I'm not sure about this product. It's okay.: Neutral sentiment
* I found this product based on the other reviews. It worked for me.: Positive sentiment

General sentiment: The reviews contain both positive and negative sentiments. Some customers expressed their love for the product, while others expressed their dislike. Neutral sentiment was also expressed by one customer. Overall, the reviews suggest that the product has the potential to elicit strong feelings from customers, both positive and negative.

Sematic Kernel packages

Create a C# console application named AIToolkitSK and add to it required packages with the following terminal window commands:

dotnet new console -n AIToolkitSK
cd AIToolkitSK
dotnet add package Microsoft.SemanticKernel

Start VS Code with:

code .

Click on the "AI Toolkit" tab in VS Code and make sure that the "Minstral 7B" model is running.

Replace content of Program.cs with this code:

using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var model = "mistral-7b-v02-int4-cpu";
var baseUrl = "http://localhost:5272/v1/";
var apikey = "unused";

// Create a chat completion service
var kernel = Kernel.CreateBuilder()
    .AddOpenAIChatCompletion(modelId: model, apiKey: apikey, endpoint: new Uri(baseUrl))
    .Build();
var chat = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage("You are a useful chatbot. Always reply in a funny way with short answers.");
var settings = new OpenAIPromptExecutionSettings
{
    MaxTokens = 500,
    Temperature = 1,
};

while (true)
{
    Console.Write("\nUser: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput)) break;

    history.AddUserMessage(userInput);

    var responseBuilder = new StringBuilder();
    Console.Write("\nAI: ");
    await foreach (var message in chat.GetStreamingChatMessageContentsAsync(userInput, settings, kernel))
    {
        responseBuilder.Append(message);
        Console.Write(message);
    }
}

This is a simple chat completion app.

Run the application with:

dotnet run

My prompt was:

Red or white wine with beef steak?

The response was:

AI:  Both red and white wines can pair well with beef steak, but a red wine is generally the more traditional choice. Red wines, such as Cabernet Sauvignon, Merlot, or Pinot Noir, have flavors that complement the rich and savory flavors of beef. However, if you prefer a lighter taste, a white wine such as Pinot Noir or Chardonnay can also work well with beef steak. Ultimately, it comes down to personal preference.

Conclusion

We have seen how to use SLMs hosted by VS Code through the AI Toolkit extension. We were able to communicate with the model from these two C# applications: (1) a app the uses OpenAI packages, and (2) an app that uses Sematic Kernel.

Monday, February 24, 2025

Using Azure OpenAI Whisper in an ASP.NET Razor Pages app

 Using Azure OpenAI, we will explore the audio-centric Whisper neural net from OpenAI. You can find more details about Whisper at https://github.com/openai/whisper.  The examples in this article assume that you have a developer account with Azure. These are the features we will explore:

  1. Transcribing audio into text
  2. Converting text into audio
  3. Translating audio from another spoken language into English text

Source Code: https://github.com/medhatelmasry/WhisperWebAzureOpenAI

Prerequisites:

  • You need a subscription with Azure.
  • The example uses Razor pages in ASP.NET 9.0
  • We will use the standard VS Code editor
  • You have installed the “C# Dev Kit” extension in VS Code

Getting Started

We will start by:

  1. creating an ASP.NET Razor Pages web app
  2. adding Azure packages to the project

Execute these commands in a terminal window: 

dotnet new razor -o WhisperWebAzureOpenAI
cd WhisperWebAzureOpenAI
dotnet add package Azure.AI.OpenAI -v 2.2.0-beta.2
dotnet add package Microsoft.Extensions.Azure

Start VS Code in the current project folder with:

code .

Add the following to appsettings.Development.json:

"AzOpenAI": {
  "Key": "YOUR-AZURE-OPENAI-KEY-HERE",
  "Url":  "YOUR-AZURE-OPENAI-ENDPOINT-HERE",
  "Audio2Text":
  {
    "Model": "whisper",
    "Folder": "audio2text"
  },
  "Text2Audio": {
    "Model": "tts",
    "Folder": "text2audio"
  },
  "Translation": {
    "Model": "whisper",
    "Folder": "translation"
  }
}

NOTE: Replace the value of the Key and Url settings above with your Azure OpenAI key and endpoint.

Model whisper is used for audio to text and audio translations. Model tts is used for converting text into audio.

Add this service to Program.cs:

builder.Services.AddAzureClients(clientBuilder =>
{
    // read key from configuration
    string? key = builder.Configuration["AzOpenAI:Key"];
    string? url = builder.Configuration["AzOpenAI:Url"];
    var credentials = new AzureKeyCredential(key!);
    
    // Register a custom client factory
    clientBuilder.AddClient<AzureOpenAIClient, AzureOpenAIClientOptions>(
        (options, _, _) => new AzureOpenAIClient(
            new Uri(url!), credentials, options)); 
});

Download a zip file from https://medhat.ca/images/audio.zip. Extract the file in the wwwroot folder.  This creates the following directory structure under wwwroot:

Note the presence of these audio files in the /wwwroot/audio/audio2text folder:

aboutSpeechSdk.wav
audio_houseplant_care.mp3
speechService.wav
TalkForAFewSeconds16.wav
wikipediaOcelot.wav

Also, note the presence of these audio files in the /wwwroot/audio/translation folder:

audio_arabic.mp3
audio_french.wav
audio_spanish.mp3

Add razor pages

In VS Code, view your project in the “Solution Explorer” tab:

Right-click on the Pages folder and add a razor page named Audio2Text:

Similarly, add these two razor pages:

  1. Text2Audio
  2. Translation

Make these code replacements into the respective files:

Audio2Text Razor Page

Audio2Text.cshtml.cs

using Azure.AI.OpenAI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace WhisperWebAzureOpenAI.Pages;
public class Audio2TextModel : PageModel {
    private readonly ILogger<Audio2TextModel> _logger;
    private readonly AzureOpenAIClient _azureOpenAIClient;
    private readonly IConfiguration _configuration;
    public List<SelectListItem>? AudioFiles { get; set; }
    public Audio2TextModel(
        ILogger<Audio2TextModel> logger, 
        AzureOpenAIClient client,
        IConfiguration configuration
    )
    {
        _logger = logger;
        _azureOpenAIClient = client;
        _configuration = configuration;
        // create wwroot/audio folder if it doesn't exist
        string? folder = _configuration["AzOpenAI:Audio2Text:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        if (!Directory.Exists(path)) {
            Directory.CreateDirectory(path);
        }
    }
    public void OnGet() {
        AudioFiles = GetAudioFiles();
    }
    public async Task<IActionResult> OnPostAsync(string? audioFile) {
        if (string.IsNullOrEmpty(audioFile)) {
            return Page();
        }
        string? deploymentName = _configuration["AzOpenAI:Audio2Text:Model"];
        var audioClient = _azureOpenAIClient.GetAudioClient(deploymentName);
        
        var result = await audioClient.TranscribeAudioAsync(audioFile);
        if (result is null) {
            return Page();
        }
        string? folder = _configuration["AzOpenAI:Audio2Text:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        ViewData["AudioFile"] = audioFile.StartsWith(path) ? audioFile.Substring(path.Length + 1) : audioFile;
        ViewData["Transcription"] = result.Value.Text;
        AudioFiles = GetAudioFiles();
        return Page();
    }
    public List<SelectListItem> GetAudioFiles() {
        List<SelectListItem> items = new List<SelectListItem>();
        string? folder = _configuration["AzOpenAI:Audio2Text:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        
        // Get files with .wav or .mp3 extensions
        string[] wavFiles = Directory.GetFiles(path, "*.wav");
        string[] mp3Files = Directory.GetFiles(path, "*.mp3");
        // Combine the arrays
        string[] list = wavFiles.Concat(mp3Files).ToArray();
        foreach (var item in list) {
            items.Add(new SelectListItem {
                Value = item.ToString(),
                Text = item.StartsWith(path) ? item.Substring(path.Length + 1) : item
            });
        }
        return items;
    }
}

Audio2Text.cshtml

@page
@model Audio2TextModel
@{ ViewData["Title"] = "Audio to Text Transcription"; }
<div class="text-center">
    <h1 class="display-4">@ViewData["Title"]</h1>
    <form method="post">
        <select asp-items="@Model.AudioFiles" name="audioFile"></select>
        <button type="submit">Submit</button>
    </form>
</div>
@if (ViewData["AudioFile"] != null) {
    <p></p>
    <h3 class="text-danger">@ViewData["AudioFile"]</h3>
}
@if (ViewData["Transcription"] != null) {
    <p class="alert alert-success">@ViewData["Transcription"]</p>
}

Text2Audio Razor Page

Text2Audio.cshtml.cs

using Azure.AI.OpenAI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OpenAI;
using OpenAI.Audio;
namespace WhisperWebAzureOpenAI.Pages;
public class Text2AudioModel : PageModel {
    private readonly ILogger<Text2AudioModel> _logger;
    private readonly AzureOpenAIClient _openAIClient;
    private readonly IConfiguration _configuration;
    const string DefaultText = @"Security officials confiscating bottles of water, tubes of 
shower gel and pots of face creams are a common sight at airport security.  
But officials enforcing the no-liquids rule at South Korea's Incheon International Airport 
have been busy seizing another outlawed item: kimchi, a concoction of salted and fermented 
vegetables that is a staple of every Korean dinner table.";
    public Text2AudioModel(ILogger<Text2AudioModel> logger,
        AzureOpenAIClient client,
        IConfiguration configuration
    )
    {
        _logger = logger;
        _openAIClient = client;
        _configuration = configuration;
        // create wwroot/audio folder if it doesn't exist
        string? folder = _configuration["AzOpenAI:Text2Audio:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        if (!Directory.Exists(path)) {
            Directory.CreateDirectory(path);
        }
    }
    public void OnGet() { 
        ViewData["sampleText"] = DefaultText;
    }
    public async Task<IActionResult> OnPostAsync(string inputText) {
        string? modelName = _configuration["AzOpenAI:Text2Audio:Model"];
        var audioClient = _openAIClient.GetAudioClient(modelName);
        BinaryData speech = await audioClient.GenerateSpeechAsync(inputText, GeneratedSpeechVoice.Alloy);
        // Generate a consistent file name based on the hash of the input text
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        byte[] hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(inputText));
        string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        string fileName = $"{hashString}.mp3";
        string? folder = _configuration["AzOpenAI:Text2Audio:Folder"];
        string filePath = Path.Combine("wwwroot", "audio", folder!, fileName);
        // Check if the file already exists
        if (!System.IO.File.Exists(filePath)) {
            using FileStream stream = System.IO.File.OpenWrite(filePath);
            speech.ToStream().CopyTo(stream);
        }
        ViewData["sampleText"] = inputText;
        ViewData["AudioFilePath"] = $"/audio/{folder}/{fileName}";
        return Page();
    }
}

Text2Audio.cshtml

@page
@model Text2AudioModel
@{ ViewData["Title"] = "Text to Audio"; }
<h1>@ViewData["Title"]</h1>
<div class="text-center">
    <form method="post">
        <label for="prompt">Enter text to convert to audio:</label>
        <br />
        <textarea type="text" name="inputText" id="inputText" cols="80" rows="5" required>@if (ViewData["sampleText"]!=null){@ViewData["sampleText"]}</textarea>
        <br /><input type="submit" value="Submit" />
    </form>
    <p></p>
    @if (ViewData["AudioFilePath"] != null) {
        <audio controls>
            <source src="@ViewData["AudioFilePath"]" type="audio/mpeg">
            Your browser does not support the audio element.
        </audio>
    }
</div>

Translation Razor Page

Translation.cshtml.cs

using Azure.AI.OpenAI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using OpenAI;
namespace WhisperWebAzureOpenAI.Pages;
public class TranslationModel : PageModel {
    private readonly ILogger<TranslationModel> _logger;
    private readonly AzureOpenAIClient _openAIClient;
    private readonly IConfiguration _configuration;
    public List<SelectListItem>? AudioFiles { get; set; }
    public TranslationModel(ILogger<TranslationModel> logger,
        AzureOpenAIClient client,
        IConfiguration configuration
    )
    {
        _logger = logger;
        _openAIClient = client;
        _configuration = configuration;
        // create wwroot/audio folder if it doesn't exist
        string? folder = _configuration["AzOpenAI:Translation:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        if (!Directory.Exists(path)) {
            Directory.CreateDirectory(path);
        }
    }
    public void OnGet() {
        AudioFiles = GetAudioFiles();
    }
    public async Task<IActionResult> OnPostAsync(string? audioFile) {
        if (string.IsNullOrEmpty(audioFile)) {
            return Page();
        }
        string? modelName = _configuration["AzOpenAI:Translation:Model"];
        var audioClient = _openAIClient.GetAudioClient(modelName);
        var result = await audioClient.TranslateAudioAsync(audioFile);
        if (result is null) {
            return Page();
        }
        
        string? folder = _configuration["AzOpenAI:Translation:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        ViewData["AudioFile"] = audioFile.StartsWith(path) ? audioFile.Substring(path.Length + 1) : audioFile;
        ViewData["Transcription"] = result.Value.Text;
        AudioFiles = GetAudioFiles();
        return Page();
    }
    public List<SelectListItem> GetAudioFiles() {
        List<SelectListItem> items = new List<SelectListItem>();
        string? folder = _configuration["AzOpenAI:Translation:Folder"];
        string? path = $"wwwroot/audio/{folder}";
        // Get files with .wav or .mp3 extensions
        string[] wavFiles = Directory.GetFiles(path, "*.wav");
        string[] mp3Files = Directory.GetFiles(path, "*.mp3");
        // Combine the arrays
        string[] list = wavFiles.Concat(mp3Files).ToArray();
        foreach (var item in list) {
            items.Add(new SelectListItem {
                Value = item.ToString(),
                Text = item.StartsWith(path) ? item.Substring(path.Length + 1) : item
            });
        }
        return items;
    }
}

Translation.cshtml

@page
@model TranslationModel
@{ ViewData["Title"] = "Audio Translation"; }
<div class="text-center">
    <h1 class="display-4">@ViewData["Title"]</h1>
    <form method="post">
        <select asp-items="@Model.AudioFiles" name="audioFile"></select>
        <button type="submit">Submit</button>
    </form>
</div>
@if (ViewData["AudioFile"] != null) {
    <p></p>
    <h3 class="text-danger">@ViewData["AudioFile"]</h3>
}
@if (ViewData["Transcription"] != null) {
    <p class="alert alert-success">@ViewData["Transcription"]</p>
}

Adding pages to menu system

Let us see our new pages in action. But first, we need to add links to the three razor pages in the menu system. Open Pages/Shared/_Layout.cshtml in the editor and add these menu items inside the <ul> . . . </ul> block:

<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-page="/Audio2Text">Audio to Text</a>
</li>
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-page="/Text2Audio">Text to Audio</a>
</li>
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-page="/Translation">Translation</a>
</li>

Let’s try it out!

Start the application by executing the following command in the terminal window:

dotnet watch

Audio to Text Page


Text to Audio Page


Translation

Bonus - Streaming audio

Going back to the Text2Audio pages, bear in mind that the audio is being saved to the server's file system then linked to the <audio ..> element. We can instead stream the audio without the need of saving a file on the server. Let us see how that works. In the Text2Audio,cshtml.cs, add the following method:

public async Task<IActionResult> OnGetSpeakAsync(string text) {
  string? modelName = _configuration["AzOpenAI:Text2Audio:Model"];
  var audioClient = _openAIClient.GetAudioClient(modelName);
  BinaryData speech = await audioClient.GenerateSpeechAsync(text, GeneratedSpeechVoice.Alloy);
  MemoryStream memoryStream = new MemoryStream();
  speech.ToStream().CopyTo(memoryStream);
  memoryStream.Position = 0; // Reset the position to the beginning of the stream
  return File(memoryStream, "audio/wav");
}

Add this code to Text2Audio,cshtml just before the closing </div> tag:

<button id="speakBtn" class="btn btn-warning">Speak</button>
<audio id="audioPlayer" type="audio/wav" ></audio>
<script>
  document.getElementById('speakBtn').addEventListener('click', function () {
    var text = encodeURIComponent(document.getElementById('inputText').value);
    fetch('/Text2Audio?handler=Speak&text=' + text)
        .then(response => response.blob())
        .then(blob => {
            var url = URL.createObjectURL(blob);
            var audioPlayer = document.getElementById('audioPlayer');
            audioPlayer.src = url;
            audioPlayer.play();
        });
  });
</script>

Run the application and view the Text2Audio pages, you will notice a new "Speak" button:



Click on the speak button and you will be able to have the audio streamed back to you.

Conclusion

With the knowledge of how to use Azure OpenAI Whisper under your belt, I am sure you will build great apps. Happy Coding.