Mit Azure OpenAI Bilder in C# generieren
Lesedauer: 7 Minuten

Microsoft ermöglicht die Möglichkeit, Bilder mithilfe von Azure OpenAI zu generieren. In diesem Beitrag werde ich dir zeigen, wie du ganz einfach die REST-API nutzen kannst, um Bilder mithilfe künstlicher Intelligenz zu generieren, indem du eine einfache Konsolenanwendung in C# schreibst, um mit deiner Azure OpenAI-Instanz zu verbinden.

Vorbereitung

Derzeit musst du Zugriff auf die Azure OpenAI-Dienste beantragen und über ein aktives Azure-Abonnement verfügen. Falls das der Fall ist, musst du das Antragsformular von Microsoft ausfüllen. Du wirst verschiedene Fragen beantworten müssen und Microsoft benötigt einige Zeit, um deinen Antrag zu genehmigen.

Azure Ressourcen

Wenn du Zugang zu den Azure OpenAI-Diensten hast, kannst du das Azure-Portal öffnen und mit dem Hinzufügen einer OpenAI-Ressource beginnen. Suche im Marketplace nach „Azure OpenAI“. Stelle sicher, dass du das Abonnement auswählst, das Zugriff auf die Azure OpenAI-Dienste hat.

Derzeit ist DALL-E nur in der Region East US verfügbar, daher achte darauf, East US auszuwählen.


Während der nächsten Schritte musst du nichts ändern. Lasse einfach die Standardwerte wie sie sind. Auf der letzten Seite klicke auf die Schaltfläche „Erstellen“.

Öffne die gerade erstellte Ressource im Azure-Portal und auf der Übersichtsseite klicke einfach auf den Link „Go to Azure OpenAI Studio“ oder die Schaltfläche „Explore„.

Im Azure AI Studio findest du auf der linken Seite den Menüpunkt „DALL-E (Preview)„. Klicke einfach darauf.

Hier kannst du den Vorgang zur Bildgenerierung problemlos in deinem Browser ausprobieren. Aber wir möchten eine benutzerdefinierte Konsolenanwendung erstellen, also klicken wir auf „View Code„, um Zugang zum API-Schlüssel und zur Endpunkt-URL zu erhalten. Von der Endpunkt-URL benötigen wir den Ressourcennamen, in meinem Fall „image-generation-demo„, und wir benötigen auch den Schlüssel. Bitte kopiere beide Werte.

Konsolenapplikation in C#

Jetzt, da wir alle Azure-Ressourcen eingerichtet haben, können wir Visual Studio starten und eine einfache Konsolenanwendung in C# implementieren, um Bilder generieren zu können.

Erstelle eine neue Konsolenanwendung. Ich werde sie AzureOpenAIImageConsole nennen, aber du kannst einen anderen Namen wählen, wenn du möchtest.

Füge das NuGet-Paket System.CommandLine zu deiner Lösung hinzu. Dieses Paket ist derzeit nur als Vorabversion verfügbar, daher stelle sicher, dass du das Kontrollkästchen „Include prerelease“ auswählst.

Erstelle einen Ordner namens „Models„. In diesem Ordner benötigen wir vier Klassen. Lass uns mit der Datei „AppConsole.cs“ beginnen.

using System.CommandLine;

namespace AzureOpenAIImageConsole.Models;

internal record class AppConsole(
    IConsole Console);

Als Nächstes erstellen wir die Klasse „AddOptions.cs“ in unserem „Models„-Ordner. Diese Klasse enthält alle verschiedenen Optionen, die wir unserem Befehl übergeben können.

using System.CommandLine;

namespace AzureOpenAIImageConsole.Models;

internal record class AppOptions(
    string AzureOpenAIResource,
    string AzureOpenAIKey,
    string DeleteId,
    string Prompt,
    int NumberOfImages,
    int ImageSize,
    string OutputFilePath,
    IConsole Console) : AppConsole(Console);

Die dritte Klasse ist die „AzureOpenAIImageRequestItem.cs„. Diese Klasse repräsentiert den Body der POST-Anforderung, um den Bildgenerierungsprozess zu starten.

using System.Text.Json.Serialization;

namespace AzureOpenAIImageConsole.Models;

internal record class AzureOpenAIImageRequestItem(
    [property: JsonPropertyName("prompt")] string Prompt,
    [property: JsonPropertyName("n")] int? AmountOfImages,
    [property: JsonPropertyName("size")] string? ImageSize);

Die letzte Klasse nennt sich „AzureOpenAIImageResponseItem.cs“ und enthält die Antwort von der REST-API.

using System.Text.Json.Serialization;

namespace AzureOpenAIImageConsole.Models;

internal record class AzureOpenAIImageResponseItem(
    [property: JsonPropertyName("created")] int Created,
    [property: JsonPropertyName("expires")] int Expires,
    [property: JsonPropertyName("id")] string Id,
    [property: JsonPropertyName("result")] Result Result,
    [property: JsonPropertyName("status")] string Status);

internal record class Result(
    [property: JsonPropertyName("data")] Data[] Data);

internal record class Data(
    [property: JsonPropertyName("url")] string Url);

Um die Lesbarkeit zu erhöhen, erstellen wir eine neue Datei „Program.Options.cs“ im Hauptverzeichnis. Wir entfernen den Namespace und markieren die Klasse als „static“ und „partial„.

using AzureOpenAIImageConsole.Models;
using System.CommandLine;
using System.CommandLine.Invocation;

internal static partial class Program
{
    private static readonly Option<string> _azureOpenAIResource =
        new(name: "--openairesource",
            description: "Resource name of your Azure OpenAI service.");

    private static readonly Option<string> _azureOpenAIKey =
        new(name: "--openaikey",
            description: "API key of your Azure OpenAI service.");

    private static readonly Option<string> _deleteId =
        new(aliases: new string[] { "--delete", "-d" },
            description: "Delete the images with the corresponding id.");

    private static readonly Option<string> _prompt =
        new(aliases: new string[] { "--prompt", "-p" },
            description: "A text description of the desired image.");

    private static readonly Option<int> _numberOfImages =
        new(aliases: new string[] { "--number", "-n" },
            description: "The number of images to generate (1 to 5).");

    private static readonly Option<int> _imageSize =
        new(aliases: new string[] { "--size", "-s" },
            description: "The desired image size (1: 256x256, 2: 512x512, 3: 1024x1024).");

    private static readonly Option<string> _outputFilePath =
        new(aliases: new string[] { "--output", "-o" },
            description: "Path to a file where the image links will be stored.");


    private static readonly RootCommand _rootCommand =
    new(description: """
        Use an Azure OpenAI service to generate an amount of images from 
        a text prompt in a desired size.
        """)
    {
            _azureOpenAIResource, _azureOpenAIKey, _deleteId, _prompt,
            _numberOfImages, _imageSize, _outputFilePath
    };

    private static AppOptions GetParsedAppOptions(InvocationContext context) =>
       new(
           AzureOpenAIResource: context.ParseResult.GetValueForOption(_azureOpenAIResource) ?? "",
           AzureOpenAIKey: context.ParseResult.GetValueForOption(_azureOpenAIKey) ?? "",
           DeleteId: context.ParseResult.GetValueForOption(_deleteId) ?? "",
           Prompt: context.ParseResult.GetValueForOption(_prompt) ?? "",
           NumberOfImages: context.ParseResult.GetValueForOption(_numberOfImages),
           ImageSize: context.ParseResult.GetValueForOption(_imageSize),
           OutputFilePath: context.ParseResult.GetValueForOption(_outputFilePath) ?? "",
           Console: context.Console);
}

Dieser Teil der Datei „Program.cs“ deklariert die verschiedenen Argumente, die wir innerhalb unseres Befehls verwenden können. Wir definieren auch unser „RootCommand“ und stellen eine Methode namens „GetParsedAppOptions“ bereit, um die Werte aus den Argumenten zu erhalten.

Nun werden wir eine weitere Datei namens „Program.Helpers.cs“ erstellen. Diese Datei enthält einige Hilfsmethoden. Zum Beispiel verwenden wir eine Methode, um einen HttpClient mit den richtigen Headern, wie dem „api-key„-Header, zu erstellen. Außerdem verwenden wir Methoden, um die richtigen Endpunkte für die verschiedenen Operationen zu generieren oder das Argument „size“ in den für die API verwendeten richtigen Wert umzuwandeln. Wir schließen auch Methoden ein, um die benötigten Informationen aus der HttpResponseMessage zu extrahieren.

using AzureOpenAIImageConsole.Models;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

internal static partial class Program
{
    private static HttpClient CreateHttpClient(
        string azureOpenAIKey)
    {
        // create httpclient
        var httpClient = new HttpClient();

        // set accept header
        httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));

        // set api-key header
        httpClient.DefaultRequestHeaders.Add(
            "api-key",
            azureOpenAIKey);

        // set useragent header
        httpClient.DefaultRequestHeaders.Add(
            "x-ms-useragent",
            "AzureOpenAIImageConsole/0.0.1");

        // return httpclient
        return httpClient;
    }

    private static HttpRequestMessage CreateHttpRequestMessage(
        string azureOpenAIResource, 
        string prompt, 
        int? amount, 
        int size)
    {
        // translate size to size string
        string sizeValue = ToSizeValue(size);

        // create requestItem
        AzureOpenAIImageRequestItem requestItem = new(
            prompt, 
            amount, 
            sizeValue);

        // serialize request body
        string requestContent = JsonSerializer.Serialize(requestItem);

        // get endpoint
        string endpoint = GetRequestEndpoint(azureOpenAIResource);

        // create HttpRequestMessage
        HttpRequestMessage request = new(HttpMethod.Post, endpoint)
        {
            Content = new StringContent(
                requestContent,
                Encoding.UTF8,
                "application/json")
        };

        // return request
        return request;
    }

    private static async Task<string?> GetGenerationIdAsync(
        HttpResponseMessage httpResponseMessage)
    {
        // validate that the request was successful
        httpResponseMessage.EnsureSuccessStatusCode();

        // read the content
        string content = await httpResponseMessage.Content
            .ReadAsStringAsync();

        // deserialize the content
        AzureOpenAIImageResponseItem? response = JsonSerializer
            .Deserialize<AzureOpenAIImageResponseItem>(content);

        // return the image creation id
        return response?.Id;
    }

    private static async Task<(bool Success, AzureOpenAIImageResponseItem? Response)> GetImageCreationResponseAsync(
        this HttpResponseMessage httpResponseMessage)
    {
        // validate that the request was successful
        httpResponseMessage.EnsureSuccessStatusCode();

        // read the content
        string content = await httpResponseMessage.Content
            .ReadAsStringAsync();

        // deserialize the content
        AzureOpenAIImageResponseItem? response = JsonSerializer
            .Deserialize<AzureOpenAIImageResponseItem>(content);

        // check if status == "succeeded"
        if (response?.Status == "succeeded")
        {
            return (true, response);
        }

        // no validate response was send
        return (false, null);
    }

    // Get the endpoint for the image request
    private static string GetRequestEndpoint(string resource)
        => $"https://{resource}.openai.azure.com/" +
            $"openai/images/generations:submit" +
            $"?api-version=2023-06-01-preview";

    // Get the endpoint for the image check
    private static string GetCheckEndpoint(string resource, string? id)
        => $"https://{resource}.openai.azure.com/" +
            $"openai/operations/images/{id}" +
            $"?api-version=2023-06-01-preview";

    // Get the endpoint for the image deletion
    private static string GetDeleteEndpoint(string resource, string? id)
        => $"https://{resource}.openai.azure.com/" +
            $"openai/operations/images/{id}" +
            $"?api-version=2023-06-01-preview";

    // Map an integer value to a concrete size string
    private static string ToSizeValue(int size)
        => size switch
        {
            1 => "256x256",
            2 => "512x512",
            3 => "1024x1024",
            _ => "512x512"
        };
}

Nun können wir mit der Implementierung der „echten“ Program.cs-Datei beginnen. Zuerst möchten wir eine kleine Validierung hinzufügen, um zu überprüfen, ob alle erforderlichen Argumente bereitgestellt wurden.

// Validate the AppOptions
_rootCommand.AddValidator(
    result =>
    {
        // Check that an Azure OpenAI Resource value is provided
        if (result.FindResultFor(_azureOpenAIResource) is null)
        {
            result.ErrorMessage +=
                $"Please provide the resource name of your Azure OpenAI service.{Environment.NewLine}";
        }
        // Check that an Azure OpenAI Key value is provided
        if (result.FindResultFor(_azureOpenAIKey) is null)
        {
            result.ErrorMessage +=
                $"Please provide the API key of your Azure OpenAI service.{Environment.NewLine}";
        }
        // Check that the --delete or -d value is provided
        if (result.FindResultFor(_deleteId) is not null)
        {
            return;
        }
        // Check that the --prompt or -p value is provided
        if (result.FindResultFor(_prompt) is null)
        {
            result.ErrorMessage +=
                $"Please provide a value for --prompt or -p.{Environment.NewLine}";
        }
        // Check that the --number or -n value is provided and in the correct range
        var numberOfImages = result.GetValueForOption(_numberOfImages);
        if (numberOfImages < 1 || numberOfImages > 5)
        {
            result.ErrorMessage +=
                $"Please provide a valid value for --numbers or -n. The value needs to be between 1 and 5.{Environment.NewLine}";
        }
        // Check that the --size or -s value is provided and in the correct range
        var imageSize = result.GetValueForOption(_imageSize);
        if (imageSize < 1 || imageSize > 3)
        {
            result.ErrorMessage +=
                $"Please provide a valid value for --size or -s. The value needs to be between 1 and 3.{Environment.NewLine}";
        }
    });

Für jeden Aufruf benötigen wir mindestens die Angaben zur Azure OpenAI-Ressource und zum Azure OpenAI-ApiKey. Wenn wir Bilder löschen möchten, müssen wir das Argument --delete oder -d bereitstellen. Wenn dies angegeben ist, können wir die Validierung beenden. Andernfalls überprüfen wir alle verschiedenen Werte und stellen sicher, dass auch der Bereich für die Größe und die Menge korrekt festgelegt ist.

Im nächsten Schritt verwenden wir die Methode SetHandler, um die Logik unseres Befehls zu definieren.

using AzureOpenAIImageConsole.Models;
using System.CommandLine;

// Set the Handler for the Commmand
_rootCommand.SetHandler(
    async (context) =>
    {
        // get options
        AppOptions options = GetParsedAppOptions(
            context);
        // create httpclient
        HttpClient httpClient = CreateHttpClient(
            options.AzureOpenAIKey);
        // check if delete id is set and delete images if necessary
        if (!string.IsNullOrEmpty(options.DeleteId))
        {
            var deleteEndpoint = GetDeleteEndpoint(
                options.AzureOpenAIResource, 
                options.DeleteId);
            var deleteResponse = await httpClient.DeleteAsync(deleteEndpoint);
            if (deleteResponse.IsSuccessStatusCode)
            {
                Console.WriteLine("Images where deleted successfully.");
            }
            return;
        }
        // create request
        HttpRequestMessage requestMessage = CreateHttpRequestMessage(
            options.AzureOpenAIResource,
            options.Prompt,
            options.NumberOfImages,
            options.ImageSize);
        // make request
        HttpResponseMessage response = await httpClient
            .SendAsync(requestMessage);
        // get id
        string? id = await GetGenerationIdAsync(response);
        Console.WriteLine();
        Console.WriteLine($"Your ID: {id}");
        // waiting...
        Console.WriteLine();
        Console.WriteLine("Image(s) will be generated...");
        // check if images are available
        var checkEndpoint = GetCheckEndpoint(
            options.AzureOpenAIResource, 
            id);
        bool isFinished = false;
        do
        {
            await Task.Delay(2000);
            HttpResponseMessage checkResponse = await httpClient
                .GetAsync(checkEndpoint);
            (bool success, AzureOpenAIImageResponseItem? imageCreationResponse) = await checkResponse.GetImageCreationResponseAsync();
            isFinished = success;
            if (success && imageCreationResponse is not null)
            {
                string urls = string.Join(
                    Environment.NewLine, 
                    imageCreationResponse.Result.Data.Select(x => x.Url));
                
                await File.AppendAllTextAsync(
                    options.OutputFilePath, 
                    $"ID: {id}{Environment.NewLine}{urls}{Environment.NewLine}{Environment.NewLine}");
                Console.WriteLine();
                Console.WriteLine("Image(s) are written to file...");
            }
        } while (!isFinished);
    });

return await _rootCommand.InvokeAsync(args);

Zuerst erhalten wir die analysierten Optionen. Als nächstes erstellen wir den HttpClient und überprüfen, ob die Löschoption angegeben ist. Wenn dies der Fall ist, löschen wir die Bilder mit Hilfe des HttpClient und beenden die Ausführung.

Wenn die Löschoption nicht angegeben ist, erstellen wir die HttpRequestMessage und senden sie an die API. Wir erhalten eine ID als Antwort, die auf der Konsole ausgegeben wird. Anschließend müssen wir regelmäßig überprüfen, ob die Bilder generiert wurden. Wenn dies der Fall ist, schreiben wir die URLs in die angegebene Ausgabedatei.

Nun können wir unseren Befehl ausprobieren. Öffne einfach eine Windows-Konsole, navigiere zum Ordner mit der csproj-Datei und verwende „dotnet build“ mit den von uns bereitgestellten Argumenten, um den Bildgenerierungsprozess zu starten.

In der bereitgestellten Ausgabe findest du Links zu einem Blob-Speicher von Microsoft, der dein generiertes Bild enthält. Dies ist das Ergebnis für meine Eingabeaufforderung:

Um die Bilder zu löschen, gib einfach die Option --delete und die ID an, die auch in die Ausgabedatei geschrieben wurde.

Fazit

In diesem Blogbeitrag haben wir eine einfache Konsolenanwendung in C# erstellt, um Bilder mithilfe eines Azure OpenAI-Dienstes zu generieren. Den Quellcode findest du in diesem GitHub-Repository.

Einen Tweet aus C# veröffentlichen Extension Methods: Collections Rekursive Funktionen in C#, Python und Racket