Creando Videos Virales con SORA en .NET MAUI

¡Hola devs! ¡Qué emoción poder ser parte del .NET MAUI UI July – 2025! Te invito a que explores todas las fabulosas publicaciones que otros autores han publicado con temas relacionados con .NET MAUI.

 

En esta publicación, te voy a mostrar cómo creé una aplicación que permite conectarse al servicio de SORA de Azure OpenAI para la generación de videos impresionantes. Este proyecto es tan solo una parte de un sistema más complejo que desarrollé hace algún tiempo que permite generar videos de YouTube de forma automatizada. En lo personal es un tema que me apasiona y se me hizo interesante poder compartirlo con ustedes. Tengo que decir además, que me apoyé mucho en el uso del agente de VS Code para tener la demo lo antes posible. ¡Comencemos!
El proyecto de generación de videos cuenta con 4 páginas principales que permiten llevar al usuario por el proceso de generación de un video. Estas páginas son las siguientes:

  • AddVideoIdeaPage.xaml: Página para ingresar el prompt y configuración del video generado
  • SettingsPage.xaml: Página para agregar las claves de los servicios de Azure OpenAI
  • VideoDisplayPlage.xaml: Página que muestra el video generado
  • VideoPromptsPage.xaml: Página que crea el prompt mejorado para la generación del video

A continuación te muestro cada una de las páginas:

Creación de la página de configuración

Comencemos con la página más sencilla, que es la de configuración de claves para conectar la app con los servicios de Azure OpenAI. En este caso, la definición de la página XAML se ve de la siguiente forma:
<ScrollView>
    <VerticalStackLayout Padding="20" Spacing="25">

        <!--  Page Header  -->
        <VerticalStackLayout Spacing="5">
            <Label
                FontAttributes="Bold"
                FontSize="28"
                HorizontalOptions="Center"
                Text="App Settings"
                TextColor="{AppThemeBinding Light={StaticResource Primary},
                                            Dark={StaticResource White}}" />

            <Label
                FontSize="14"
                HorizontalOptions="Center"
                Text="Configure your AI services connections"
                TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                            Dark={StaticResource Gray300}}" />
        </VerticalStackLayout>

        <!--  Azure LLM Configuration Section  -->
        <Border
            Padding="20"
            BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                                Dark={StaticResource Gray900}}"
            Stroke="{AppThemeBinding Light={StaticResource Gray200},
                                        Dark={StaticResource Gray600}}"
            StrokeShape="RoundRectangle 12"
            StrokeThickness="1">

            <VerticalStackLayout Spacing="15">

                <!--  Section Header  -->
                <Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="10">
                    <Border
                        Grid.Column="0"
                        BackgroundColor="{AppThemeBinding Light={StaticResource Primary},
                                                            Dark={StaticResource Primary}}"
                        HeightRequest="32"
                        StrokeShape="RoundRectangle 8"
                        VerticalOptions="Center"
                        WidthRequest="32">
                        <Label
                            FontSize="16"
                            HorizontalOptions="Center"
                            Text="🧠"
                            TextColor="White"
                            VerticalOptions="Center" />
                    </Border>

                    <VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
                        <Label
                            FontAttributes="Bold"
                            FontSize="18"
                            Text="Azure LLM Service"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                        <Label
                            FontSize="12"
                            Text="Configure your Azure Language Model connection"
                            TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                        Dark={StaticResource Gray400}}" />
                    </VerticalStackLayout>

                    <Button
                        Grid.Column="2"
                        BackgroundColor="Transparent"
                        BorderColor="{AppThemeBinding Light={StaticResource Primary},
                                                        Dark={StaticResource White}}"
                        BorderWidth="1"
                        Command="{Binding TestAzureLlmConnectionCommand}"
                        CornerRadius="8"
                        FontSize="12"
                        HeightRequest="32"
                        IsEnabled="{Binding IsAzureLlmConfigComplete}"
                        Text="Test"
                        TextColor="{AppThemeBinding Light={StaticResource Primary},
                                                    Dark={StaticResource White}}"
                        WidthRequest="60" />
                </Grid>

                <!--  Endpoint Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Endpoint URL"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Entry
                            FontSize="14"
                            Placeholder="https://your-resource.openai.azure.com/"
                            PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                Dark={StaticResource Gray500}}"
                            Text="{Binding AzureLlmEndpoint}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </VerticalStackLayout>

                <!--  API Key Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="API Key"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Grid ColumnDefinitions="*,Auto">
                            <Entry
                                Grid.Column="0"
                                FontSize="14"
                                IsPassword="{Binding IsAzureLlmApiKeyVisible, Converter={StaticResource InvertedBoolConverter}}"
                                Placeholder="Enter your Azure API key"
                                PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                    Dark={StaticResource Gray500}}"
                                Text="{Binding AzureLlmApiKey}"
                                TextColor="{AppThemeBinding Light={StaticResource Black},
                                                            Dark={StaticResource White}}" />

                            <Button
                                Grid.Column="1"
                                BackgroundColor="Transparent"
                                Command="{Binding ToggleAzureLlmApiKeyVisibilityCommand}"
                                FontSize="16"
                                Padding="5"
                                Text="{Binding AzureLlmApiKeyToggleIcon}"
                                TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                            Dark={StaticResource Gray400}}" />
                        </Grid>
                    </Border>
                </VerticalStackLayout>

                <!--  Deployment Name Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Deployment Name"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Entry
                            FontSize="14"
                            Placeholder="gpt-4, gpt-35-turbo, etc."
                            PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                Dark={StaticResource Gray500}}"
                            Text="{Binding AzureLlmDeployment}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </VerticalStackLayout>

            </VerticalStackLayout>
        </Border>

        <!--  SORA Configuration Section  -->
        <Border
            Padding="20"
            BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                                Dark={StaticResource Gray900}}"
            Stroke="{AppThemeBinding Light={StaticResource Gray200},
                                        Dark={StaticResource Gray600}}"
            StrokeShape="RoundRectangle 12"
            StrokeThickness="1">

            <VerticalStackLayout Spacing="15">

                <!--  Section Header  -->
                <Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="10">
                    <Border
                        Grid.Column="0"
                        BackgroundColor="{AppThemeBinding Light={StaticResource Primary},
                                                            Dark={StaticResource Primary}}"
                        HeightRequest="32"
                        StrokeShape="RoundRectangle 8"
                        VerticalOptions="Center"
                        WidthRequest="32">
                        <Label
                            FontSize="16"
                            HorizontalOptions="Center"
                            Text="🎬"
                            TextColor="White"
                            VerticalOptions="Center" />
                    </Border>

                    <VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
                        <Label
                            FontAttributes="Bold"
                            FontSize="18"
                            Text="SORA Video Service"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                        <Label
                            FontSize="12"
                            Text="Configure your SORA video generation connection"
                            TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                        Dark={StaticResource Gray400}}" />
                    </VerticalStackLayout>

                    <Button
                        Grid.Column="2"
                        BackgroundColor="Transparent"
                        BorderColor="{AppThemeBinding Light={StaticResource Primary},
                                                        Dark={StaticResource White}}"
                        BorderWidth="1"
                        Command="{Binding TestSoraConnectionCommand}"
                        CornerRadius="8"
                        FontSize="12"
                        HeightRequest="32"
                        IsEnabled="{Binding IsSoraConfigComplete}"
                        Text="Test"
                        TextColor="{AppThemeBinding Light={StaticResource Primary},
                                                    Dark={StaticResource White}}"
                        WidthRequest="60" />
                </Grid>

                <!--  Endpoint Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Endpoint URL"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Entry
                            FontSize="14"
                            Placeholder="https://api.openai.com/v1/sora/"
                            PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                Dark={StaticResource Gray500}}"
                            Text="{Binding SoraEndpoint}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </VerticalStackLayout>

                <!--  API Key Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="API Key"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Grid ColumnDefinitions="*,Auto">
                            <Entry
                                Grid.Column="0"
                                FontSize="14"
                                IsPassword="{Binding IsSoraApiKeyVisible, Converter={StaticResource InvertedBoolConverter}}"
                                Placeholder="Enter your SORA API key"
                                PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                    Dark={StaticResource Gray500}}"
                                Text="{Binding SoraApiKey}"
                                TextColor="{AppThemeBinding Light={StaticResource Black},
                                                            Dark={StaticResource White}}" />

                            <Button
                                Grid.Column="1"
                                BackgroundColor="Transparent"
                                Command="{Binding ToggleSoraApiKeyVisibilityCommand}"
                                FontSize="16"
                                Padding="5"
                                Text="{Binding SoraApiKeyToggleIcon}"
                                TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                            Dark={StaticResource Gray400}}" />
                        </Grid>
                    </Border>
                </VerticalStackLayout>

                <!--  Deployment Name Field  -->
                <VerticalStackLayout Spacing="5">
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Model Name"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Border
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                    Dark={StaticResource Gray500}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Entry
                            FontSize="14"
                            Placeholder="sora-1.0, sora-turbo, etc."
                            PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                                Dark={StaticResource Gray500}}"
                            Text="{Binding SoraDeployment}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </VerticalStackLayout>

            </VerticalStackLayout>
        </Border>

        <!--  Action Buttons  -->
        <Grid ColumnDefinitions="*,*" ColumnSpacing="15" Margin="0,10,0,0">

            <Button
                Grid.Column="0"
                BackgroundColor="Transparent"
                BorderColor="#DC3545"
                BorderWidth="2"
                Command="{Binding ClearSettingsCommand}"
                CornerRadius="25"
                FontAttributes="Bold"
                FontSize="16"
                HeightRequest="50"
                Text="Clear All"
                TextColor="#DC3545" />

            <Button
                Grid.Column="1"
                BackgroundColor="{StaticResource Primary}"
                Command="{Binding SaveSettingsCommand}"
                CornerRadius="25"
                FontAttributes="Bold"
                FontSize="16"
                HeightRequest="50"
                IsEnabled="{Binding IsSaving, Converter={StaticResource InvertedBoolConverter}}"
                Text="Save Settings"
                TextColor="White" />

        </Grid>

        <!--  Loading Indicator  -->
        <VerticalStackLayout IsVisible="{Binding IsSaving}" 
                        Padding="20"
                        HorizontalOptions="Center" 
                        VerticalOptions="Center"
                        Spacing="15">
            
            <ActivityIndicator IsRunning="{Binding IsSaving}"
                                Color="{StaticResource Primary}"
                                HeightRequest="40"
                                WidthRequest="40"
                                HorizontalOptions="Center" />
            
            <Label Text="Saving Configuration..." 
                    FontSize="16" 
                    FontAttributes="Bold" 
                    HorizontalOptions="Center" 
                    TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
            
            <Label Text="Validating and storing your settings" 
                    FontSize="14" 
                    HorizontalOptions="Center" 
                    TextColor="{AppThemeBinding Light={StaticResource Gray600}, Dark={StaticResource Gray400}}" />
            
        </VerticalStackLayout>

    </VerticalStackLayout>
</ScrollView> 
Esta página está enlazada a un ViewModel llamado SettingsViewModel, que se ve de la siguiente manera:
public partial class SettingsViewModel : ObservableObject
{
    // Constants for Preferences keys
    private const string AZURE_LLM_ENDPOINT_KEY = "AzureLlmEndpoint";
    private const string AZURE_LLM_API_KEY = "AzureLlmApiKey";
    private const string AZURE_LLM_DEPLOYMENT_KEY = "AzureLlmDeployment";
    private const string SORA_ENDPOINT_KEY = "SoraEndpoint";
    private const string SORA_API_KEY = "SoraApiKey";
    private const string SORA_DEPLOYMENT_KEY = "SoraDeployment";

    [ObservableProperty]
    private string azureLlmEndpoint = string.Empty;

    [ObservableProperty]
    private string azureLlmApiKey = string.Empty;

    [ObservableProperty]
    private string azureLlmDeployment = string.Empty;

    [ObservableProperty]
    private string soraEndpoint = string.Empty;

    [ObservableProperty]
    private string soraApiKey = string.Empty;

    [ObservableProperty]
    private string soraDeployment = string.Empty;

    [ObservableProperty]
    private bool isAzureLlmApiKeyVisible = false;

    [ObservableProperty]
    private bool isSoraApiKeyVisible = false;

    [ObservableProperty]
    private string azureLlmApiKeyToggleIcon = "👁️";

    [ObservableProperty]
    private string soraApiKeyToggleIcon = "👁️";

    [ObservableProperty]
    private bool isSaving = false;

    // Reference to the page for displaying alerts
    private Page? _page;

    public SettingsViewModel()
    {
        LoadSettings();
    }

    public void SetPage(Page page)
    {
        _page = page;
    }

    /// <summary>
    /// Loads saved settings from Preferences
    /// </summary>
    public void LoadSettings()
    {
        AzureLlmEndpoint = Preferences.Default.Get(AZURE_LLM_ENDPOINT_KEY, string.Empty);
        AzureLlmApiKey = Preferences.Default.Get(AZURE_LLM_API_KEY, string.Empty);
        AzureLlmDeployment = Preferences.Default.Get(AZURE_LLM_DEPLOYMENT_KEY, string.Empty);
        
        SoraEndpoint = Preferences.Default.Get(SORA_ENDPOINT_KEY, string.Empty);
        SoraApiKey = Preferences.Default.Get(SORA_API_KEY, string.Empty);
        SoraDeployment = Preferences.Default.Get(SORA_DEPLOYMENT_KEY, string.Empty);
    }

    /// <summary>
    /// Command to toggle visibility of Azure LLM API key
    /// </summary>
    [RelayCommand]
    private void ToggleAzureLlmApiKeyVisibility()
    {
        IsAzureLlmApiKeyVisible = !IsAzureLlmApiKeyVisible;
        AzureLlmApiKeyToggleIcon = IsAzureLlmApiKeyVisible ? "🙈" : "👁️";
    }

    /// <summary>
    /// Command to toggle visibility of SORA API key
    /// </summary>
    [RelayCommand]
    private void ToggleSoraApiKeyVisibility()
    {
        IsSoraApiKeyVisible = !IsSoraApiKeyVisible;
        SoraApiKeyToggleIcon = IsSoraApiKeyVisible ? "🙈" : "👁️";
    }

    /// <summary>
    /// Command to save all settings
    /// </summary>
    [RelayCommand]
    private async Task SaveSettings()
    {
        IsSaving = true;

        try
        {
            // Validate settings before saving
            if (!ValidateSettings())
            {
                return;
            }

            // Save Azure LLM settings
            Preferences.Default.Set(AZURE_LLM_ENDPOINT_KEY, AzureLlmEndpoint.Trim());
            Preferences.Default.Set(AZURE_LLM_API_KEY, AzureLlmApiKey.Trim());
            Preferences.Default.Set(AZURE_LLM_DEPLOYMENT_KEY, AzureLlmDeployment.Trim());

            // Save SORA settings
            Preferences.Default.Set(SORA_ENDPOINT_KEY, SoraEndpoint.Trim());
            Preferences.Default.Set(SORA_API_KEY, SoraApiKey.Trim());
            Preferences.Default.Set(SORA_DEPLOYMENT_KEY, SoraDeployment.Trim());

            if (_page != null)
            {
                await _page.DisplayAlert("Settings Saved", "All settings have been saved successfully!", "OK");
            }
        }
        catch (Exception ex)
        {
            if (_page != null)
            {
                await _page.DisplayAlert("Error", $"Failed to save settings: {ex.Message}", "OK");
            }
        }
        finally
        {
            IsSaving = false;
        }
    }

    /// <summary>
    /// Command to clear all settings
    /// </summary>
    [RelayCommand]
    private async Task ClearSettings()
    {
        if (_page != null)
        {
            bool confirmed = await _page.DisplayAlert(
                "Clear Settings",
                "Are you sure you want to clear all settings? This action cannot be undone.",
                "Clear",
                "Cancel");

            if (!confirmed)
                return;
        }

        try
        {
            // Clear from Preferences
            Preferences.Default.Remove(AZURE_LLM_ENDPOINT_KEY);
            Preferences.Default.Remove(AZURE_LLM_API_KEY);
            Preferences.Default.Remove(AZURE_LLM_DEPLOYMENT_KEY);
            Preferences.Default.Remove(SORA_ENDPOINT_KEY);
            Preferences.Default.Remove(SORA_API_KEY);
            Preferences.Default.Remove(SORA_DEPLOYMENT_KEY);

            // Clear from UI
            AzureLlmEndpoint = string.Empty;
            AzureLlmApiKey = string.Empty;
            AzureLlmDeployment = string.Empty;
            SoraEndpoint = string.Empty;
            SoraApiKey = string.Empty;
            SoraDeployment = string.Empty;

            if (_page != null)
            {
                await _page.DisplayAlert("Settings Cleared", "All settings have been cleared successfully!", "OK");
            }
        }
        catch (Exception ex)
        {
            if (_page != null)
            {
                await _page.DisplayAlert("Error", $"Failed to clear settings: {ex.Message}", "OK");
            }
        }
    }

    /// <summary>
    /// Command to test Azure LLM connection
    /// </summary>
    [RelayCommand]
    private async Task TestAzureLlmConnection()
    {
        if (string.IsNullOrWhiteSpace(AzureLlmEndpoint) || 
            string.IsNullOrWhiteSpace(AzureLlmApiKey) || 
            string.IsNullOrWhiteSpace(AzureLlmDeployment))
        {
            if (_page != null)
            {
                await _page.DisplayAlert("Incomplete Configuration", 
                    "Please fill in all Azure LLM fields before testing.", "OK");
            }
            return;
        }

        // TODO: Implement actual connection test
        if (_page != null)
        {
            await _page.DisplayAlert("Test Connection", 
                "Azure LLM connection test functionality will be implemented soon!", "OK");
        }
    }

    /// <summary>
    /// Command to test SORA connection
    /// </summary>
    [RelayCommand]
    private async Task TestSoraConnection()
    {
        if (string.IsNullOrWhiteSpace(SoraEndpoint) || 
            string.IsNullOrWhiteSpace(SoraApiKey) || 
            string.IsNullOrWhiteSpace(SoraDeployment))
        {
            if (_page != null)
            {
                await _page.DisplayAlert("Incomplete Configuration", 
                    "Please fill in all SORA fields before testing.", "OK");
            }
            return;
        }

        // TODO: Implement actual connection test
        if (_page != null)
        {
            await _page.DisplayAlert("Test Connection", 
                "SORA connection test functionality will be implemented soon!", "OK");
        }
    }

    /// <summary>
    /// Validates all settings before saving
    /// </summary>
    private bool ValidateSettings()
    {
        var errors = new List<string>();

        // Validate Azure LLM settings
        if (string.IsNullOrWhiteSpace(AzureLlmEndpoint))
            errors.Add("Azure LLM Endpoint is required");
        else if (!Uri.TryCreate(AzureLlmEndpoint, UriKind.Absolute, out _))
            errors.Add("Azure LLM Endpoint must be a valid URL");

        if (string.IsNullOrWhiteSpace(AzureLlmApiKey))
            errors.Add("Azure LLM API Key is required");

        if (string.IsNullOrWhiteSpace(AzureLlmDeployment))
            errors.Add("Azure LLM Deployment name is required");

        // Validate SORA settings
        if (string.IsNullOrWhiteSpace(SoraEndpoint))
            errors.Add("SORA Endpoint is required");
        else if (!Uri.TryCreate(SoraEndpoint, UriKind.Absolute, out _))
            errors.Add("SORA Endpoint must be a valid URL");

        if (string.IsNullOrWhiteSpace(SoraApiKey))
            errors.Add("SORA API Key is required");

        if (string.IsNullOrWhiteSpace(SoraDeployment))
            errors.Add("SORA Deployment name is required");

        if (errors.Count > 0 && _page != null)
        {
            Task.Run(async () => await _page.DisplayAlert("Validation Error", 
                string.Join("\n", errors), "OK"));
            return false;
        }

        return true;
    }

    /// <summary>
    /// Checks if all required settings are configured
    /// </summary>
    public bool AreSettingsComplete =>
        !string.IsNullOrWhiteSpace(AzureLlmEndpoint) &&
        !string.IsNullOrWhiteSpace(AzureLlmApiKey) &&
        !string.IsNullOrWhiteSpace(AzureLlmDeployment) &&
        !string.IsNullOrWhiteSpace(SoraEndpoint) &&
        !string.IsNullOrWhiteSpace(SoraApiKey) &&
        !string.IsNullOrWhiteSpace(SoraDeployment);

    /// <summary>
    /// Checks if Azure LLM settings are complete
    /// </summary>
    public bool IsAzureLlmConfigComplete =>
        !string.IsNullOrWhiteSpace(AzureLlmEndpoint) &&
        !string.IsNullOrWhiteSpace(AzureLlmApiKey) &&
        !string.IsNullOrWhiteSpace(AzureLlmDeployment);

    /// <summary>
    /// Checks if SORA settings are complete
    /// </summary>
    public bool IsSoraConfigComplete =>
        !string.IsNullOrWhiteSpace(SoraEndpoint) &&
        !string.IsNullOrWhiteSpace(SoraApiKey) &&
        !string.IsNullOrWhiteSpace(SoraDeployment);
} 
Al estar utilizando MVVM Toolkit la tarea se ha simplificado bastante. De igual forma, puedes ver que hago uso de la clase `Preferences` para almacenar estas claves y leerlas en caso de que existan previamente.

El resultado de esta página se puede ver a continuación:
La página Settings

Creación de la página de parámetros para la generación del video

La segunda página creada se llama AddVideoIdeaPage.xaml. Esta página está pensada para que el usuario pueda ingresar la idea de su video virarl a ser generado:
La caja del prompt donde el usuario describe el video que quiere para SORA
Algo que es muy común es que los usuarios ingresen prompts sencillos que no describen muy bien lo que el generador de videos debe producir. Para solventar la problemática, agregué un checkbox que permite decidir al usuario si quiere mejorar el prompt por medio de IA, lo que invocará el servicio de Chat para mejorar el prompt ingresado:
Checkbox para habilitar mejoramiento del prompt por IA
De igual forma, es en esta misma página donde el usuario puede configurar el video de salida, ya que SORA permite diferentes opciones de configuración para crear videos en horizontal o vertical, diferente resolución e incluso duración de los videos generados:
Configuración para el video resultante de la generación por SORA
El código de esta página es un poco más complejo:
<ScrollView>
    <VerticalStackLayout
        Padding="30,0"
        Spacing="25"
        VerticalOptions="Center">

        <!--  Title  -->
        <Label
            FontAttributes="Bold"
            FontSize="24"
            HorizontalOptions="Center"
            Text="Share Your Viral Video Idea"
            TextColor="{AppThemeBinding Light={StaticResource Primary},
                                        Dark={StaticResource White}}" />

        <!--  Subtitle  -->
        <Label
            FontSize="16"
            HorizontalOptions="Center"
            Text="Describe your creative idea for a viral video"
            TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                        Dark={StaticResource Gray300}}" />

        <!--  Text Input Area  -->
        <Border
            Padding="10"
            BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                              Dark={StaticResource Gray950}}"
            Stroke="{AppThemeBinding Light={StaticResource Gray200},
                                     Dark={StaticResource Gray500}}"
            StrokeShape="RoundRectangle 8"
            StrokeThickness="1">

            <Editor
                BackgroundColor="Transparent"
                FontSize="16"
                HeightRequest="200"
                Placeholder="Tell us about your viral video idea... What makes it special? What's the concept, story, or hook that will capture viewers' attention?"
                PlaceholderColor="{AppThemeBinding Light={StaticResource Gray400},
                                                   Dark={StaticResource Gray500}}"
                Text="{Binding VideoIdea}"
                TextColor="{AppThemeBinding Light={StaticResource Black},
                                            Dark={StaticResource White}}" />

        </Border>

        <!--  Character count label  -->
        <Label
            FontSize="12"
            HorizontalOptions="End"
            Text="{Binding CharacterCountText}"
            TextColor="{AppThemeBinding Light={StaticResource Gray500},
                                        Dark={StaticResource Gray400}}" />

        <!--  AI Enhancement Option  -->
        <Border
            Margin="0,10,0,0"
            Padding="15"
            BackgroundColor="{AppThemeBinding Light={StaticResource Gray100},
                                              Dark={StaticResource Gray900}}"
            Stroke="{AppThemeBinding Light={StaticResource Gray200},
                                     Dark={StaticResource Gray500}}"
            StrokeShape="RoundRectangle 12"
            StrokeThickness="1">

            <Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="12">

                <!--  AI Enhancement Icon  -->
                <Border
                    Grid.Column="0"
                    BackgroundColor="{AppThemeBinding Light={StaticResource Primary},
                                                      Dark={StaticResource Primary}}"
                    HeightRequest="32"
                    HorizontalOptions="Center"
                    StrokeShape="RoundRectangle 8"
                    VerticalOptions="Center"
                    WidthRequest="32">

                    <Label
                        FontSize="16"
                        HorizontalOptions="Center"
                        Text="✨"
                        TextColor="White"
                        VerticalOptions="Center" />
                </Border>

                <!--  Enhancement Description  -->
                <StackLayout
                    Grid.Column="1"
                    Spacing="2"
                    VerticalOptions="Center">
                    <Label
                        FontAttributes="Bold"
                        FontSize="16"
                        Text="AI Prompt Enhancement"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />

                    <Label
                        FontSize="13"
                        Text="Let AI optimize your idea for better viral potential"
                        TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                    Dark={StaticResource Gray400}}" />
                </StackLayout>

                <!--  Enhancement Toggle  -->
                <Border
                    Grid.Column="2"
                    BackgroundColor="{Binding EnhancementToggleBorderColor}"
                    HeightRequest="30"
                    StrokeShape="RoundRectangle 20"
                    VerticalOptions="Center"
                    WidthRequest="50">

                    <Border.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding ToggleEnhancementCommand}" />
                    </Border.GestureRecognizers>

                    <Grid>
                        <!--  Toggle Circle  -->
                        <Border
                            Margin="2,0"
                            BackgroundColor="White"
                            HeightRequest="26"
                            HorizontalOptions="Start"
                            StrokeShape="RoundRectangle 14"
                            TranslationX="{Binding EnhancementToggleTranslationX}"
                            VerticalOptions="Center"
                            WidthRequest="26">

                            <Label
                                FontSize="12"
                                HorizontalOptions="Center"
                                Opacity="{Binding EnhancementToggleOpacity}"
                                Text="{Binding EnhancementToggleIcon}"
                                VerticalOptions="Center" />
                        </Border>
                    </Grid>
                </Border>

            </Grid>
        </Border>

        <!--  Video Configuration Section  -->
        <Border
            Margin="0,20,0,0"
            Padding="20"
            BackgroundColor="{AppThemeBinding Light={StaticResource Gray100},
                                              Dark={StaticResource Gray900}}"
            Stroke="{AppThemeBinding Light={StaticResource Gray200},
                                     Dark={StaticResource Gray500}}"
            StrokeShape="RoundRectangle 12"
            StrokeThickness="1">

            <StackLayout Spacing="20">

                <!--  Configuration Header  -->
                <StackLayout Orientation="Horizontal" Spacing="10">
                    <Border
                        BackgroundColor="{AppThemeBinding Light={StaticResource Primary},
                                                          Dark={StaticResource Primary}}"
                        HeightRequest="28"
                        StrokeShape="RoundRectangle 6"
                        VerticalOptions="Center"
                        WidthRequest="28">
                        <Label
                            FontSize="14"
                            HorizontalOptions="Center"
                            Text="🎬"
                            TextColor="White"
                            VerticalOptions="Center" />
                    </Border>

                    <Label
                        FontAttributes="Bold"
                        FontSize="18"
                        Text="Video Configuration"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}"
                        VerticalOptions="Center" />
                </StackLayout>

                <!--  Resolution Selection  -->
                <StackLayout>
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Resolution"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />
                    <Label
                        FontSize="12"
                        Text="Select video quality level - higher resolution provides better quality but takes longer to generate"
                        TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                    Dark={StaticResource Gray400}}" />

                    <Border
                        Margin="0,8,0,0"
                        BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                                          Dark={StaticResource Gray950}}"
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                 Dark={StaticResource Gray600}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Picker
                            Title="Select Resolution"
                            BackgroundColor="Transparent"
                            FontSize="16"
                            ItemsSource="{Binding ResolutionOptions}"
                            SelectedItem="{Binding SelectedResolution}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </StackLayout>

                <!--  Duration Selection  -->
                <StackLayout>
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Duration (seconds)"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />
                    <Label
                        FontSize="12"
                        Text="Longer videos take more time to generate"
                        TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                    Dark={StaticResource Gray400}}" />

                    <Border
                        Margin="0,8,0,0"
                        BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                                          Dark={StaticResource Gray950}}"
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                 Dark={StaticResource Gray600}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Picker
                            Title="Select Duration"
                            BackgroundColor="Transparent"
                            FontSize="16"
                            ItemsSource="{Binding DurationOptions}"
                            SelectedItem="{Binding SelectedDuration}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </StackLayout>

                <!--  Orientation Selection  -->
                <StackLayout>
                    <Label
                        FontAttributes="Bold"
                        FontSize="14"
                        Text="Orientation"
                        TextColor="{AppThemeBinding Light={StaticResource Black},
                                                    Dark={StaticResource White}}" />
                    <Label
                        FontSize="12"
                        Text="Choose aspect ratio: Horizontal (16:9), Vertical (9:16), or Square (1:1)"
                        TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                    Dark={StaticResource Gray400}}" />

                    <Border
                        Margin="0,8,0,0"
                        BackgroundColor="{AppThemeBinding Light={StaticResource White},
                                                          Dark={StaticResource Gray950}}"
                        Stroke="{AppThemeBinding Light={StaticResource Gray300},
                                                 Dark={StaticResource Gray600}}"
                        StrokeShape="RoundRectangle 8"
                        StrokeThickness="1">
                        <Picker
                            Title="Select Orientation"
                            BackgroundColor="Transparent"
                            FontSize="16"
                            ItemsSource="{Binding OrientationOptions}"
                            SelectedItem="{Binding SelectedOrientation}"
                            TextColor="{AppThemeBinding Light={StaticResource Black},
                                                        Dark={StaticResource White}}" />
                    </Border>
                </StackLayout>

            </StackLayout>
        </Border>

        <!--  Generate Button  -->
        <Button
            Margin="0,20,0,0"
            BackgroundColor="{StaticResource Primary}"
            Command="{Binding GenerateViralStoryCommand}"
            CornerRadius="25"
            FontAttributes="Bold"
            FontSize="18"
            HeightRequest="50"
            HorizontalOptions="Fill"
            IsEnabled="{Binding CanGenerateStory}"
            Text="{Binding GenerateButtonText}"
            TextColor="White" />

        <!--  Enhanced Loading Indicator  -->
        <StackLayout
            Margin="0,20,0,0"
            HorizontalOptions="Center"
            IsVisible="{Binding IsGenerating}"
            Spacing="20"
            VerticalOptions="Center">

            <ActivityIndicator
                HeightRequest="50"
                HorizontalOptions="Center"
                IsRunning="{Binding IsGenerating}"
                WidthRequest="50"
                Color="{StaticResource Primary}" />

            <StackLayout Spacing="10">
                <Label
                    FontAttributes="Bold"
                    FontSize="18"
                    HorizontalOptions="Center"
                    HorizontalTextAlignment="Center"
                    Text="🧠 Processing your idea..."
                    TextColor="{AppThemeBinding Light={StaticResource Primary},
                                                Dark={StaticResource Primary}}" />

                <Label
                    FontSize="14"
                    HorizontalOptions="Center"
                    HorizontalTextAlignment="Center"
                    LineBreakMode="WordWrap"
                    Text="We’re enhancing your idea with AI and generating optimized prompts to create an amazing viral video."
                    TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                                Dark={StaticResource Gray400}}" />

                <Label
                    FontAttributes="Italic"
                    FontSize="12"
                    HorizontalOptions="Center"
                    HorizontalTextAlignment="Center"
                    Text="⏱️ This process may take a few moments"
                    TextColor="{AppThemeBinding Light={StaticResource Gray500},
                                                Dark={StaticResource Gray500}}" />
            </StackLayout>

        </StackLayout>


    </VerticalStackLayout>
</ScrollView> 
Por otra parte, el ViewModel llamado AddVideoIdeaViewModel se ve de la siguiente forma:
public partial class AddVideoIdeaViewModel : ObservableObject
{
    private readonly IChatService _chatService;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(CanGenerateStory))]
    private string videoIdea = string.Empty;

    [ObservableProperty]
    private string characterCountText = "0 characters";

    [ObservableProperty]
    private bool isEnhancementEnabled = false;

    [ObservableProperty]
    private string generateButtonText = "Generate Viral Story";

    [ObservableProperty]
    private string enhancementToggleIcon = "🤖";

    [ObservableProperty]
    private double enhancementToggleOpacity = 0.5;

    [ObservableProperty]
    private double enhancementToggleTranslationX = 0.0;

    [ObservableProperty]
    private Color enhancementToggleBorderColor = Colors.Gray;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(CanGenerateStory))]
    private bool isGenerating = false;

    // Video Configuration Properties
    [ObservableProperty]
    private string selectedResolution = "720";

    [ObservableProperty]
    private string selectedDuration = "10";

    [ObservableProperty]
    private string selectedOrientation = "Horizontal";

    // Available options for pickers
    public List<string> ResolutionOptions { get; } = ["480", "720", "1080"];
    public List<string> DurationOptions { get; } = ["5", "10", "15", "20"];
    public List<string> OrientationOptions { get; } = ["Horizontal", "Vertical", "Square"];

    // Reference to the page for displaying alerts
    private Page? _page;

    public AddVideoIdeaViewModel(IChatService chatService)
    {
        _chatService = chatService;
        CheckChatServiceConfiguration();
    }

    public void SetPage(Page page)
    {
        _page = page;
    }

    /// <summary>
    /// Checks if chat service is configured and updates UI accordingly
    /// </summary>
    private async void CheckChatServiceConfiguration()
    {
        try
        {
            var isConfigured = await _chatService.IsConfiguredAsync();
            if (!isConfigured)
            {
                // Update UI to indicate that enhancement requires configuration
                EnhancementToggleIcon = "⚙️";
            }
        }
        catch
        {
            // If checking fails, assume not configured
            EnhancementToggleIcon = "⚙️";
        }
    }

    /// <summary>
    /// Updates character count when video idea text changes
    /// </summary>
    partial void OnVideoIdeaChanged(string value)
    {
        var characterCount = string.IsNullOrEmpty(value) ? 0 : value.Length;
        CharacterCountText = $"{characterCount} characters";
    }

    /// <summary>
    /// Updates UI elements when enhancement toggle state changes
    /// </summary>
    partial void OnIsEnhancementEnabledChanged(bool value)
    {
        UpdateGenerateButtonText();
        UpdateToggleAppearance();
    }

    /// <summary>
    /// Command to toggle AI enhancement feature
    /// </summary>
    [RelayCommand]
    private async Task ToggleEnhancement()
    {
        IsEnhancementEnabled = !IsEnhancementEnabled;
        await AnimateToggle();
    }

    /// <summary>
    /// Command to generate viral story from user's idea
    /// </summary>
    [RelayCommand]
    private async Task GenerateViralStory()
    {
        if (string.IsNullOrWhiteSpace(VideoIdea))
        {
            if (_page != null)
            {
                await _page.DisplayAlert("Empty Idea", "Please enter your video idea before generating a viral story.", "OK");
            }
            return;
        }

        IsGenerating = true;

        try
        {
            string finalVideoIdea = VideoIdea;

            // Check if enhancement is enabled and service is configured
            if (IsEnhancementEnabled)
            {
                var isConfigured = await _chatService.IsConfiguredAsync();
                if (!isConfigured)
                {
                    if (_page != null)
                    {
                        var result = await _page.DisplayAlert(
                            "Service Not Configured", 
                            "AI enhancement requires Azure LLM configuration. Would you like to go to Settings to configure it?", 
                            "Go to Settings", 
                            "Continue without Enhancement");
                        
                        if (result)
                        {
                            await Shell.Current.GoToAsync("//Settings");
                            return;
                        }
                        else
                        {
                            // Continue without enhancement
                            IsEnhancementEnabled = false;
                            await AnimateToggle();
                        }
                    }
                }
                else
                {
                    // Enhance the idea using Azure LLM
                    try
                    {
                        finalVideoIdea = await _chatService.EnhancePromptAsync(VideoIdea);
                        
                        // No DisplayAlert - keep loading while processing
                        Debug.WriteLine($"[AddVideoIdea] Idea enhanced successfully: {finalVideoIdea}");
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"[AddVideoIdea] Enhancement failed: {ex.Message}");
                        if (_page != null)
                        {
                            await _page.DisplayAlert("Enhancement Failed", 
                                $"Failed to enhance the idea: {ex.Message}\n\nContinuing with original idea.", 
                                "OK");
                        }
                    }
                }
            }

            // TODO: Store the final video idea and enhancement status for the VideoPromptsPage
            // Pass the data through navigation parameters including video configuration
            var (width, height) = GetVideoDimensions();
            var duration = int.Parse(SelectedDuration);
            
            var navigationParameters = new Dictionary<string, object>
            {
                ["VideoIdea"] = VideoIdea,
                ["EnhancedIdea"] = finalVideoIdea ?? string.Empty,
                ["WasEnhanced"] = (finalVideoIdea != VideoIdea && !string.IsNullOrEmpty(finalVideoIdea)).ToString(),
                ["VideoWidth"] = width.ToString(),
                ["VideoHeight"] = height.ToString(),
                ["VideoDuration"] = duration.ToString(),
                ["VideoOrientation"] = SelectedOrientation
            };

            // Navigate to VideoPromptsPage after processing
            await Shell.Current.GoToAsync("//VideoPrompts", navigationParameters);
        }
        finally
        {
            IsGenerating = false;
        }
    }

    /// <summary>
    /// Updates the generate button text based on enhancement state
    /// </summary>
    private void UpdateGenerateButtonText()
    {
        GenerateButtonText = IsEnhancementEnabled 
            ? "✨ Enhance & Generate" 
            : "Generate Viral Story";
    }

    /// <summary>
    /// Updates toggle visual appearance based on current state
    /// </summary>
    private void UpdateToggleAppearance()
    {
        if (IsEnhancementEnabled)
        {
            EnhancementToggleIcon = "✨";
            EnhancementToggleOpacity = 1.0;
            EnhancementToggleTranslationX = 20.0;
            EnhancementToggleBorderColor = Color.FromRgb(0x00, 0xFF, 0x33); // Green
        }
        else
        {
            EnhancementToggleIcon = "🤖";
            EnhancementToggleOpacity = 0.5;
            EnhancementToggleTranslationX = 0.0;
            EnhancementToggleBorderColor = Colors.Gray;
        }
    }

    /// <summary>
    /// Animates the enhancement toggle switch
    /// </summary>
    private async Task AnimateToggle()
    {
        var animation = new Animation();
        var targetTranslation = IsEnhancementEnabled ? 20.0 : 0.0;
        var startTranslation = IsEnhancementEnabled ? 0.0 : 20.0;

        // Translation animation
        animation.Add(0, 1, new Animation(v => 
        {
            EnhancementToggleTranslationX = v;
        }, startTranslation, targetTranslation));

        // Color animation
        animation.Add(0, 1, new Animation(v => 
        {
            if (IsEnhancementEnabled)
            {
                var color = Color.FromRgba(
                    (int)(0x66 + (0x00 - 0x66) * v), // Red component
                    (int)(0x66 + (0xFF - 0x66) * v), // Green component  
                    (int)(0x66 + (0x33 - 0x66) * v), // Blue component
                    255);
                EnhancementToggleBorderColor = color;
            }
            else
            {
                var grayValue = (int)(0x66 + (0xCC - 0x66) * v);
                EnhancementToggleBorderColor = Color.FromRgb(grayValue, grayValue, grayValue);
            }
        }, 0, 1));

        var completionSource = new TaskCompletionSource<bool>();

        if (_page != null)
        {
            animation.Commit(_page, "ToggleAnimation", 16, 250, 
                Easing.CubicOut, (v, c) => completionSource.SetResult(true));
        }
        else
        {
            completionSource.SetResult(true);
        }

        await completionSource.Task;
    }

    /// <summary>
    /// Validates if the current idea is ready for generation
    /// </summary>
    public bool CanGenerateStory => !string.IsNullOrWhiteSpace(VideoIdea) && !IsGenerating;

    /// <summary>
    /// Gets video dimensions based on selected resolution and orientation
    /// Using SORA-supported resolution combinations
    /// </summary>
    /// <returns>Tuple with width and height values</returns>
    private (int width, int height) GetVideoDimensions()
    {
        // SORA supported resolutions: (480, 480), (854, 480), (720, 720), (1280, 720), (1080, 1080), (1920, 1080)
        return SelectedOrientation switch
        {
            "Horizontal" => SelectedResolution switch
            {
                "480" => (854, 480),    // Horizontal 480p
                "720" => (1280, 720),   // Horizontal 720p (HD)
                "1080" => (1920, 1080), // Horizontal 1080p (Full HD)
                _ => (1280, 720)        // Default to 720p horizontal
            },
            "Vertical" => SelectedResolution switch
            {
                "480" => (480, 854),    // Vertical 480p
                "720" => (720, 1280),   // Vertical 720p
                "1080" => (1080, 1920), // Vertical 1080p
                _ => (720, 1280)        // Default to 720p vertical
            },
            "Square" => SelectedResolution switch
            {
                "480" => (480, 480),    // Square 480p
                "720" => (720, 720),    // Square 720p
                "1080" => (1080, 1080), // Square 1080p
                _ => (720, 720)         // Default to 720p square
            },
            _ => (1280, 720)            // Default to horizontal 720p
        };
    }
} 
Ahora, hablemos de la página que permite modificar el prompt mejorado para la generación del video.

La página de configuración del prompt final

Tanto si el usuario seleccionó o no la opción para mejorar el video, éste será redirigido a la página VideoPromptsPage.xaml. Esta página tiene una serie de mini prompts que serán añadidos al prompt final para crear un video sorprendente. Estos mini prompts pueden ser modificados si así se desea y básicamente son una serie de prompts que definen algunas características para crear videos virales.

De igual forma, esta página es la que se encarga de generar el video, por lo que pasar de esta a la siguiente puede ser un poco tardado debido a la cantidad gigantesca de procesamiento que conlleva generar un video.

La página se ve de la siguiente forma:
Página de configuración del prompt final
El código XAML de esta página es relativamente sencillo, pudiendo ser modificado para agregar otras características que pudiéramos necesitar:
<!--  Grid to allow overlay  -->
<Grid>
    <!--  Main Content  -->
    <ScrollView>
        <StackLayout Padding="20" Spacing="15">

            <!--  Header Section  -->
            <Border BackgroundColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" StrokeThickness="0">
                <Border.StrokeShape>
                    <RoundRectangle CornerRadius="12" />
                </Border.StrokeShape>
                <StackLayout Padding="20" Spacing="10">
                    <Label
                        FontAttributes="Bold"
                        FontSize="24"
                        Text="Generated Prompts"
                        TextColor="White" />
                    <Label
                        FontSize="16"
                        Opacity="0.9"
                        Text="{Binding VideoIdea}"
                        TextColor="White" />
                </StackLayout>
            </Border>

            <!--  Statistics  -->
            <Grid ColumnDefinitions="*,*" ColumnSpacing="10">
                <Border
                    Grid.Column="0"
                    BackgroundColor="{AppThemeBinding Light=#F0F0F0,
                                                        Dark=#2A2A2A}"
                    StrokeThickness="0">
                    <Border.StrokeShape>
                        <RoundRectangle CornerRadius="8" />
                    </Border.StrokeShape>
                    <StackLayout Padding="15" Spacing="5">
                        <Label
                            FontAttributes="Bold"
                            FontSize="24"
                            HorizontalOptions="Center"
                            Text="{Binding TotalPrompts}" />
                        <Label
                            FontSize="12"
                            HorizontalOptions="Center"
                            Opacity="0.7"
                            Text="Total Prompts" />
                    </StackLayout>
                </Border>

                <Border
                    Grid.Column="1"
                    BackgroundColor="{AppThemeBinding Light=#F0F0F0,
                                                        Dark=#2A2A2A}"
                    StrokeThickness="0">
                    <Border.StrokeShape>
                        <RoundRectangle CornerRadius="8" />
                    </Border.StrokeShape>
                    <StackLayout Padding="15" Spacing="5">
                        <Label
                            FontAttributes="Bold"
                            FontSize="24"
                            HorizontalOptions="Center"
                            Text="{Binding EditedPrompts}"
                            TextColor="{AppThemeBinding Light={StaticResource Primary},
                                                        Dark={StaticResource PrimaryDark}}" />
                        <Label
                            FontSize="12"
                            HorizontalOptions="Center"
                            Opacity="0.7"
                            Text="Edited" />
                    </StackLayout>
                </Border>
            </Grid>

            <!--  Prompts List  -->
            <CollectionView ItemsSource="{Binding Prompts}" SelectionMode="None">
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="viewmodels:VideoPrompt">
                        <Border
                            Margin="0,5"
                            BackgroundColor="{AppThemeBinding Light=White,
                                                                Dark=#1A1A1A}"
                            Stroke="{AppThemeBinding Light=#E0E0E0,
                                                        Dark=#404040}"
                            StrokeThickness="1">
                            <Border.StrokeShape>
                                <RoundRectangle CornerRadius="12" />
                            </Border.StrokeShape>
                            <StackLayout Padding="15" Spacing="10">

                                <!--  Header with title and edited indicator  -->
                                <Grid ColumnDefinitions="*,Auto">
                                    <Label
                                        Grid.Column="0"
                                        FontAttributes="Bold"
                                        FontSize="18"
                                        Text="{Binding Title}" />
                                    <Label
                                        Grid.Column="1"
                                        FontSize="12"
                                        IsVisible="{Binding IsEdited}"
                                        Text="✏️ Edited"
                                        TextColor="{AppThemeBinding Light={StaticResource Primary},
                                                                    Dark={StaticResource PrimaryDark}}" />
                                </Grid>

                                <!--  Content  -->
                                <Label
                                    FontSize="16"
                                    LineBreakMode="WordWrap"
                                    Text="{Binding Content}" />

                                <!--  Action Buttons  -->
                                <Grid
                                    Margin="0,10,0,0"
                                    ColumnDefinitions="*,*,*"
                                    ColumnSpacing="10">
                                    <Button
                                        Grid.Column="0"
                                        BackgroundColor="{AppThemeBinding Light={StaticResource Primary},
                                                                            Dark={StaticResource PrimaryDark}}"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:VideoPromptsViewModel}}, Path=EditPromptCommand}"
                                        CommandParameter="{Binding .}"
                                        FontSize="14"
                                        Text="Edit"
                                        TextColor="White" />

                                    <Button
                                        Grid.Column="1"
                                        BackgroundColor="#FF9500"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:VideoPromptsViewModel}}, Path=RegeneratePromptCommand}"
                                        CommandParameter="{Binding .}"
                                        FontSize="14"
                                        Text="🔄 Regenerate"
                                        TextColor="White" />

                                    <Button
                                        Grid.Column="2"
                                        BackgroundColor="#FF3B30"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodels:VideoPromptsViewModel}}, Path=DeletePromptCommand}"
                                        CommandParameter="{Binding .}"
                                        FontSize="14"
                                        Text="🗑️"
                                        TextColor="White" />
                                </Grid>
                            </StackLayout>
                        </Border>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>

            <!--  Generate Video Button  -->
            <Button
                Margin="0,20,0,0"
                BackgroundColor="{AppThemeBinding Light={StaticResource Success},
                                                    Dark={StaticResource Success}}"
                Command="{Binding GenerateVideoCommand}"
                FontAttributes="Bold"
                FontSize="18"
                HeightRequest="50"
                Text="🎬 Generate Video"
                TextColor="White" />

        </StackLayout>
    </ScrollView>

    <!--  FULLSCREEN BLOCKING OVERLAY  -->
    <ContentView
        BackgroundColor="#80000000"
        InputTransparent="False"
        IsVisible="{Binding IsLoading}">

        <!--  Centered Loading Content  -->
        <StackLayout
            HorizontalOptions="Center"
            Spacing="30"
            VerticalOptions="Center">

            <!--  Loading Animation  -->
            <ActivityIndicator
                HeightRequest="80"
                IsRunning="{Binding IsLoading}"
                WidthRequest="80"
                Color="White" />

            <!--  Loading Card  -->
            <Border
                Padding="30"
                BackgroundColor="White"
                HorizontalOptions="Center"
                StrokeThickness="0">
                <Border.StrokeShape>
                    <RoundRectangle CornerRadius="15" />
                </Border.StrokeShape>

                <StackLayout
                    HorizontalOptions="Center"
                    Spacing="20"
                    WidthRequest="300">

                    <!--  Main Message  -->
                    <Label
                        FontAttributes="Bold"
                        FontSize="20"
                        HorizontalOptions="Center"
                        HorizontalTextAlignment="Center"
                        Text="🎬 Generating Video with AI"
                        TextColor="{StaticResource Primary}" />

                    <!--  Progress Message  -->
                    <Label
                        FontSize="16"
                        HorizontalOptions="Center"
                        HorizontalTextAlignment="Center"
                        LineBreakMode="WordWrap"
                        Text="Your viral video is being created using SORA AI. This process may take several minutes due to the complexity of video generation."
                        TextColor="{StaticResource Gray600}" />

                    <!--  Status Message  -->
                    <Label
                        FontAttributes="Italic"
                        FontSize="14"
                        HorizontalOptions="Center"
                        HorizontalTextAlignment="Center"
                        Text="🔄 Processing..."
                        TextColor="{StaticResource Primary}" />

                    <!--  Warning Message  -->
                    <Label
                        FontAttributes="Bold"
                        FontSize="12"
                        HorizontalOptions="Center"
                        HorizontalTextAlignment="Center"
                        Text="⚠️ Do not close the app during this process"
                        TextColor="#FF9500" />

                </StackLayout>

            </Border>
        </StackLayout>
    </ContentView>

</Grid> 
El ViewModel de esta página, llamado VideoPromptsViewModel se ve de la siguiente forma:
public partial class VideoPromptsViewModel : ObservableObject, IQueryAttributable
{
    private readonly IChatService _chatService;
    private readonly ISoraService _soraService;

    [ObservableProperty]
    private ObservableCollection<VideoPrompt> prompts = new();

    [ObservableProperty]
    private int totalPrompts = 0;

    [ObservableProperty]
    private int editedPrompts = 0;

    [ObservableProperty]
    private string videoIdea = string.Empty;

    [ObservableProperty]
    private bool isLoading = false;

    // Store original and enhanced ideas
    private string _originalIdea = string.Empty;
    private string _enhancedIdea = string.Empty;
    private bool _wasEnhanced = false;

    // Video configuration properties
    private int _videoWidth = 1280;      // Default horizontal 720p
    private int _videoHeight = 720;
    private int _videoDuration = 10;     // Default 10 seconds
    private string _videoOrientation = "Horizontal";

    public VideoPromptsViewModel(IChatService chatService, ISoraService soraService)
    {
        _chatService = chatService;
        _soraService = soraService;
        // Don't load sample data by default anymore
    }

    // IQueryAttributable implementation
    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.ContainsKey("VideoIdea"))
        {
            _originalIdea = query["VideoIdea"].ToString() ?? "";
        }

        if (query.ContainsKey("EnhancedIdea"))
        {
            _enhancedIdea = query["EnhancedIdea"].ToString() ?? "";
            _wasEnhanced = !string.IsNullOrEmpty(_enhancedIdea);
        }

        if (query.ContainsKey("WasEnhanced"))
        {
            _wasEnhanced = bool.Parse(query["WasEnhanced"].ToString() ?? "false");
        }

        // Get video configuration parameters
        if (query.ContainsKey("VideoWidth"))
        {
            int.TryParse(query["VideoWidth"].ToString(), out _videoWidth);
        }

        if (query.ContainsKey("VideoHeight"))
        {
            int.TryParse(query["VideoHeight"].ToString(), out _videoHeight);
        }

        if (query.ContainsKey("VideoDuration"))
        {
            int.TryParse(query["VideoDuration"].ToString(), out _videoDuration);
        }

        if (query.ContainsKey("VideoOrientation"))
        {
            _videoOrientation = query["VideoOrientation"].ToString() ?? "Horizontal";
        }

        // Set the display idea (enhanced if available, otherwise original)
        VideoIdea = _wasEnhanced && !string.IsNullOrEmpty(_enhancedIdea) ? _enhancedIdea : _originalIdea;

        // Generate prompts based on the video idea
        _ = GeneratePromptsFromAI();
    }

    private async Task GeneratePromptsFromAI()
    {
        if (string.IsNullOrWhiteSpace(VideoIdea))
        {
            LoadSampleData(); // Fallback to sample data
            return;
        }

        IsLoading = true;
        Prompts.Clear();

        try
        {
            var isConfigured = await _chatService.IsConfiguredAsync();
            if (!isConfigured)
            {
                LoadSampleData(); // Fallback to sample data
                return;
            }

            // Generate prompts using AI
            var generatedPrompts = await _chatService.GenerateVideoPromptsAsync(VideoIdea, _wasEnhanced);
            
            int promptId = 1;
            foreach (var prompt in generatedPrompts)
            {
                Prompts.Add(new VideoPrompt
                {
                    Id = promptId++,
                    Title = prompt.Title,
                    Content = prompt.Content,
                    IsEdited = false
                });
            }

            UpdateStatistics();
        }
        catch (Exception)
        {
            // If AI generation fails, fall back to sample data
            LoadSampleData();
        }
        finally
        {
            IsLoading = false;
        }
    }

    private void LoadSampleData()
    {
        VideoIdea = "Dancing cat compilation with trending music";
        
        Prompts.Add(new VideoPrompt
        {
            Id = 1,
            Title = "Opening Hook",
            Content = "Start with the most energetic cat dance move synchronized to the beat drop",
            IsEdited = false
        });

        Prompts.Add(new VideoPrompt
        {
            Id = 2,
            Title = "Main Content",
            Content = "Sequence of 5-7 different cats dancing to trending audio, each for 3-4 seconds",
            IsEdited = false
        });

        Prompts.Add(new VideoPrompt
        {
            Id = 3,
            Title = "Engagement Boost",
            Content = "Add text overlay: 'Which cat has the best moves? 👇 Comment below!'",
            IsEdited = false
        });

        Prompts.Add(new VideoPrompt
        {
            Id = 4,
            Title = "Call to Action",
            Content = "End with 'Follow for more dancing pets!' with heart and dancing emojis",
            IsEdited = false
        });

        UpdateStatistics();
    }

    [RelayCommand]
    private async Task EditPrompt(VideoPrompt prompt)
    {
        try
        {
            var page = GetCurrentPage();
            if (page == null) return;

            var result = await page.DisplayPromptAsync(
                "Edit Prompt", 
                $"Edit the {prompt.Title}:", 
                "Save", 
                "Cancel", 
                placeholder: prompt.Content, 
                initialValue: prompt.Content);

            if (!string.IsNullOrEmpty(result) && result != prompt.Content)
            {
                if (!prompt.IsEdited)
                {
                    prompt.IsEdited = true;
                    EditedPrompts++;
                }
                prompt.Content = result;
            }
        }
        catch
        {
            // Handle any navigation errors silently
        }
    }

    [RelayCommand]
    private void DeletePrompt(VideoPrompt prompt)
    {
        if (prompt.IsEdited)
        {
            EditedPrompts--;
        }
        Prompts.Remove(prompt);
        UpdateStatistics();
    }

    [RelayCommand]
    private async Task RegeneratePrompt(VideoPrompt prompt)
    {
        // Simulate regeneration
        await Task.Delay(1000);
        
        var regeneratedContent = prompt.Title switch
        {
            "Opening Hook" => "Begin with an unexpected cat appearing from behind furniture, dancing immediately",
            "Main Content" => "Create a montage of cats from different breeds, each showcasing unique dance styles",
            "Engagement Boost" => "Add interactive text: 'Tap ❤️ if your cat can dance too!'",
            "Call to Action" => "Conclude with 'Tag a friend who loves dancing cats!' with music note emojis",
            _ => "Regenerated prompt content"
        };

        if (!prompt.IsEdited)
        {
            prompt.IsEdited = true;
            EditedPrompts++;
        }
        prompt.Content = regeneratedContent;
    }

    [RelayCommand]
    private async Task GenerateVideo()
    {
        try
        {
            Debug.WriteLine("[VideoPrompts] GenerateVideo started");
            var page = GetCurrentPage();
            
            // Check if SORA service is configured
            var isConfigured = await _soraService.IsConfiguredAsync();
            Debug.WriteLine($"[VideoPrompts] SORA configured: {isConfigured}");
            
            if (!isConfigured)
            {
                Debug.WriteLine("[VideoPrompts] SORA not configured, showing alert");
                if (page != null)
                {
                    await page.DisplayAlert("Configuration Required", 
                        "Please configure your SORA credentials in the settings page.", 
                        "OK");
                }
                return;
            }

            // Activate loading UI (blocks all interaction)
            IsLoading = true;
            Debug.WriteLine("[VideoPrompts] Loading UI activated - blocking all interaction");

            // Generate the combined prompt from all prompts
            var combinedPrompt = string.Join(". ", Prompts.Select(p => p.Content));
            if (string.IsNullOrWhiteSpace(combinedPrompt))
            {
                combinedPrompt = VideoIdea; // Fallback to original idea
            }

            Debug.WriteLine($"[VideoPrompts] Combined prompt: {combinedPrompt}");
            Debug.WriteLine($"[VideoPrompts] Video configuration: {_videoWidth}x{_videoHeight}, {_videoDuration}s, {_videoOrientation}");
            Debug.WriteLine("[VideoPrompts] Starting SORA video generation...");

            // Generate video using SORA service with custom configuration
            var videoFilePath = await _soraService.GenerateVideoAsync(combinedPrompt, _videoWidth, _videoHeight, _videoDuration);
            
            Debug.WriteLine($"[VideoPrompts] ✅ Video generated successfully: {videoFilePath}");
            Debug.WriteLine($"[VideoPrompts] File exists: {File.Exists(videoFilePath)}");
            Debug.WriteLine($"[VideoPrompts] File size: {new FileInfo(videoFilePath).Length / 1024 / 1024:F2} MB");

            // Prepare navigation parameters
            var navigationParams = new Dictionary<string, object>
            {
                ["VideoTitle"] = VideoIdea,
                ["VideoSource"] = videoFilePath,
                ["OriginalPrompt"] = _originalIdea,
                ["EnhancedPrompt"] = _wasEnhanced ? _enhancedIdea : "",
                ["EnglishContent"] = combinedPrompt,
                ["HasEnhancedPrompt"] = _wasEnhanced.ToString()
            };

            Debug.WriteLine("[VideoPrompts] Navigation parameters:");
            foreach (var kvp in navigationParams)
            {
                Debug.WriteLine($"[VideoPrompts]   {kvp.Key}: {kvp.Value}");
            }

            // Navigate to video display page with real video
            Debug.WriteLine("[VideoPrompts] Navigating to VideoDisplay page...");
            await Shell.Current.GoToAsync("VideoDisplay", navigationParams);

            // Show success message
            if (page != null)
            {
                Debug.WriteLine("[VideoPrompts] Showing success message");
                await page.DisplayAlert("Video Generated!", 
                    $"Your viral video has been created successfully.\n\nFile: {Path.GetFileName(videoFilePath)}", 
                    "Watch Video");
            }
            
            Debug.WriteLine("[VideoPrompts] GenerateVideo completed successfully");
        }
        catch (InvalidOperationException ex)
        {
            Debug.WriteLine($"[VideoPrompts] Configuration error: {ex.Message}");
            var page = GetCurrentPage();
            if (page != null)
            {
                await page.DisplayAlert("Configuration Error", ex.Message, "OK");
            }
        }
        catch (TimeoutException)
        {
            Debug.WriteLine("[VideoPrompts] Timeout error");
            var page = GetCurrentPage();
            if (page != null)
            {
                await page.DisplayAlert("Timeout", 
                    "Video generation took too long. Please try again.", 
                    "OK");
            }
        }
        catch (HttpRequestException ex)
        {
            Debug.WriteLine($"[VideoPrompts] HTTP error: {ex.Message}");
            var page = GetCurrentPage();
            if (page != null)
            {
                await page.DisplayAlert("Connection Error", 
                    $"Failed to connect to the SORA service: {ex.Message}", 
                    "OK");
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"[VideoPrompts] Unexpected error: {ex.Message}");
            Debug.WriteLine($"[VideoPrompts] Exception type: {ex.GetType().Name}");
            Debug.WriteLine($"[VideoPrompts] Stack trace: {ex.StackTrace}");
            var page = GetCurrentPage();
            if (page != null)
            {
                await page.DisplayAlert("Error", $"An error occurred while generating the video: {ex.Message}", "OK");
            }
        }
        finally
        {
            // Always deactivate loading UI regardless of success or failure
            IsLoading = false;
            Debug.WriteLine("[VideoPrompts] Loading UI deactivated - interaction restored");
        }
    }


    private Page? GetCurrentPage()
    {
        try
        {
            return Application.Current?.Windows?.FirstOrDefault()?.Page;
        }
        catch
        {
            return null;
        }
    }

    private void UpdateStatistics()
    {
        TotalPrompts = Prompts.Count;
        EditedPrompts = Prompts.Count(p => p.IsEdited);
    }
}

public partial class VideoPrompt : ObservableObject
{
    [ObservableProperty]
    private int id;

    [ObservableProperty]
    private string title = string.Empty;

    [ObservableProperty]
    private string content = string.Empty;

    [ObservableProperty]
    private bool isEdited;
} 

Página con el resultado de la Generación del Video Final

La última página de la aplicación es la que muestra el video generado llamada `VideoDisplayPage.xaml`. Esta página utiliza el flamante y mejorado MediaElement, el cual es un súper control multiplataforma que permite mostrar elementos multimedia de forma muy sencila desarrollado por la comunidad .NET. De igual forma, dentro de esta página he dejado algunos elementos como placeholders, que permitirían la regeneración del video, compartir el video en redes sociales o descargar el video, que puedes adaptar de acuerdo a tus necesidades.
La página de visualización de videos generados por el modelo SORA
El código de esta página es el siguiente:
<ScrollView>
    <VerticalStackLayout Padding="20" Spacing="15">

        <!--  Loading Indicator  -->
        <VerticalStackLayout
            Padding="30"
            HorizontalOptions="Center"
            IsVisible="{Binding IsLoading}"
            Spacing="20"
            VerticalOptions="Center">

            <ActivityIndicator
                HeightRequest="50"
                HorizontalOptions="Center"
                IsRunning="{Binding IsLoading}"
                WidthRequest="50"
                Color="{StaticResource Primary}" />

            <Label
                FontAttributes="Bold"
                FontSize="18"
                HorizontalOptions="Center"
                Text="Loading your generated video..."
                TextColor="{AppThemeBinding Light={StaticResource Primary},
                                            Dark={StaticResource Primary}}" />

            <Label
                FontSize="14"
                HorizontalOptions="Center"
                Text="Please wait while we prepare your video for playback"
                TextColor="{AppThemeBinding Light={StaticResource Gray600},
                                            Dark={StaticResource Gray400}}" />

        </VerticalStackLayout>

        <!--  Main Content (hidden while loading)  -->
        <VerticalStackLayout IsVisible="{Binding IsLoading, Converter={StaticResource InvertedBoolConverter}}" Spacing="15">

            <!--  Video Title  -->
            <Label
                FontAttributes="Bold"
                FontSize="18"
                HorizontalOptions="Center"
                Text="{Binding VideoTitle}"
                TextColor="White" />

            <!--  MediaElement for video playback  -->
            <Border
                Padding="5"
                BackgroundColor="Black"
                Stroke="Gray"
                StrokeThickness="2">
                <Border.StrokeShape>
                    <RoundRectangle CornerRadius="10" />
                </Border.StrokeShape>

                <toolkit:MediaElement
                    x:Name="VideoPlayer"
                    Aspect="AspectFit"
                    BackgroundColor="Black"
                    HeightRequest="300"
                    ShouldAutoPlay="False"
                    ShouldLoopPlayback="False"
                    ShouldShowPlaybackControls="True"
                    Source="{Binding VideoSource}" />
            </Border>

            <!--  Video Controls  -->
            <HorizontalStackLayout HorizontalOptions="Center" Spacing="20">
                <Button
                    BackgroundColor="#1E90FF"
                    Command="{Binding PlayCommand}"
                    Text="Play"
                    TextColor="White"
                    WidthRequest="80" />
                <Button
                    BackgroundColor="#FF4500"
                    Command="{Binding PauseCommand}"
                    Text="Pause"
                    TextColor="White"
                    WidthRequest="80" />
                <Button
                    BackgroundColor="#DC143C"
                    Command="{Binding StopCommand}"
                    Text="Stop"
                    TextColor="White"
                    WidthRequest="80" />
            </HorizontalStackLayout>

            <!--  Video Information  -->
            <Border
                Padding="15"
                BackgroundColor="#2C2C2C"
                Stroke="Gray"
                StrokeShape="RoundRectangle 10">
                <VerticalStackLayout Spacing="10">
                    <Label
                        FontAttributes="Bold"
                        FontSize="16"
                        Text="Video Information"
                        TextColor="White" />

                    <Label
                        FontSize="14"
                        LineBreakMode="WordWrap"
                        Text="{Binding VideoDescription}"
                        TextColor="LightGray" />

                    <Label
                        FontSize="12"
                        Text="{Binding VideoDuration, StringFormat='Duration: {0}'}"
                        TextColor="LightGray" />

                    <Label
                        FontSize="12"
                        Text="{Binding VideoStatus, StringFormat='Status: {0}'}"
                        TextColor="LightGray" />
                </VerticalStackLayout>
            </Border>

            <!--  Original Prompt Section  -->
            <Border
                Padding="15"
                BackgroundColor="#2C2C2C"
                Stroke="Gray"
                StrokeShape="RoundRectangle 10">
                <VerticalStackLayout Spacing="10">
                    <Label
                        FontAttributes="Bold"
                        FontSize="16"
                        Text="Original Prompt"
                        TextColor="White" />

                    <Label
                        FontSize="14"
                        LineBreakMode="WordWrap"
                        Text="{Binding OriginalPrompt}"
                        TextColor="LightGray" />
                </VerticalStackLayout>
            </Border>

            <!--  Enhanced Prompt Section (if available)  -->
            <Border
                Padding="15"
                BackgroundColor="#2C2C2C"
                IsVisible="{Binding HasEnhancedPrompt}"
                Stroke="Gray"
                StrokeShape="RoundRectangle 10">
                <VerticalStackLayout Spacing="10">
                    <Label
                        FontAttributes="Bold"
                        FontSize="16"
                        Text="AI Enhanced Prompt"
                        TextColor="White" />

                    <Label
                        FontSize="14"
                        LineBreakMode="WordWrap"
                        Text="{Binding EnhancedPrompt}"
                        TextColor="LightGray" />
                </VerticalStackLayout>
            </Border>

            <!--  English Content Section  -->
            <Border
                Padding="15"
                BackgroundColor="#2C2C2C"
                Stroke="Gray"
                StrokeShape="RoundRectangle 10">
                <VerticalStackLayout Spacing="10">
                    <Label
                        FontAttributes="Bold"
                        FontSize="16"
                        Text="English Content"
                        TextColor="White" />

                    <Label
                        FontSize="14"
                        LineBreakMode="WordWrap"
                        Text="{Binding EnglishContent}"
                        TextColor="LightGray" />
                </VerticalStackLayout>
            </Border>

            <!--  Action Buttons  -->
            <HorizontalStackLayout HorizontalOptions="Center" Spacing="15">
                <Button
                    BackgroundColor="#32CD32"
                    Command="{Binding ShareVideoCommand}"
                    Text="Share Video"
                    TextColor="White"
                    WidthRequest="120" />
                <Button
                    BackgroundColor="#4169E1"
                    Command="{Binding DownloadCommand}"
                    Text="Download"
                    TextColor="White"
                    WidthRequest="120" />
                <Button
                    BackgroundColor="#696969"
                    Command="{Binding BackCommand}"
                    Text="Back"
                    TextColor="White"
                    WidthRequest="120" />
            </HorizontalStackLayout>

        </VerticalStackLayout>
        <!--  Closing main content StackLayout  -->

    </VerticalStackLayout>
</ScrollView> 
El código del ViewModel es el siguiente:
public partial class VideoDisplayViewModel : ObservableObject, IQueryAttributable
{
    private MediaElement? _mediaElement;

    [ObservableProperty]
    private string videoTitle = "Generated Viral Video";

    [ObservableProperty]
    private string videoSource = "";

    [ObservableProperty]
    private string videoDescription = "AI-generated viral video content";

    [ObservableProperty]
    private string videoDuration = "Loading...";

    [ObservableProperty]
    private string videoStatus = "Ready";

    [ObservableProperty]
    private string originalPrompt = "";

    [ObservableProperty]
    private string enhancedPrompt = "";

    [ObservableProperty]
    private string englishContent = "";

    [ObservableProperty]
    private bool hasEnhancedPrompt = false;

    [ObservableProperty]
    private bool isLoading = true;

    public VideoDisplayViewModel()
    {
        // Sample data for demonstration
        VideoTitle = "AI Generated Viral Video";
        VideoDescription = "This video was generated using AI based on your creative prompt and enhanced with advanced algorithms.";
        OriginalPrompt = "A cat playing with a ball of yarn";
        EnhancedPrompt = "A playful orange tabby cat with bright green eyes gracefully pouncing and rolling around with a large, fluffy ball of rainbow-colored yarn in a cozy, sunlit living room with wooden floors and comfortable furniture.";
        EnglishContent = "Watch as this adorable cat brings joy and entertainment through its playful interaction with colorful yarn. Perfect for social media sharing and guaranteed to make viewers smile!";
        HasEnhancedPrompt = !string.IsNullOrEmpty(EnhancedPrompt);
        
        // For demo purposes, you can use a sample video URL or local file
        // VideoSource = "https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4";
    }

    public void SetMediaElement(MediaElement mediaElement)
    {
        _mediaElement = mediaElement;
        
        // Subscribe to MediaElement events
        if (_mediaElement != null)
        {
            _mediaElement.MediaOpened += OnMediaOpened;
            _mediaElement.MediaEnded += OnMediaEnded;
            _mediaElement.MediaFailed += OnMediaFailed;
        }
    }

    private void OnMediaOpened(object? sender, EventArgs e)
    {
        if (_mediaElement != null)
        {
            VideoDuration = _mediaElement.Duration.ToString(@"mm\:ss");
            VideoStatus = "Ready to play";
            IsLoading = false; // Stop loading when media is ready
        }
    }

    private void OnMediaEnded(object? sender, EventArgs e)
    {
        VideoStatus = "Playback completed";
    }

    private void OnMediaFailed(object? sender, EventArgs e)
    {
        VideoStatus = "Error loading video";
        IsLoading = false; // Stop loading on error
    }

    [RelayCommand]
    private void Play()
    {
        try
        {
            _mediaElement?.Play();
            VideoStatus = "Playing";
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error playing video: {ex.Message}");
            VideoStatus = "Error playing video";
        }
    }

    [RelayCommand]
    private void Pause()
    {
        try
        {
            _mediaElement?.Pause();
            VideoStatus = "Paused";
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error pausing video: {ex.Message}");
        }
    }

    [RelayCommand]
    private void Stop()
    {
        try
        {
            _mediaElement?.Stop();
            VideoStatus = "Stopped";
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error stopping video: {ex.Message}");
        }
    }

    [RelayCommand]
    private async Task ShareVideo()
    {
        try
        {
            if (!string.IsNullOrEmpty(VideoSource))
            {
                await Shell.Current.DisplayAlert("Share Video", $"Sharing: {VideoTitle}\n\nCheck out this AI-generated viral video!", "OK");
            }
            else
            {
                await Shell.Current.DisplayAlert("Info", "No video available to share", "OK");
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error sharing video: {ex.Message}");
            await Shell.Current.DisplayAlert("Error", "Unable to share video", "OK");
        }
    }

    [RelayCommand]
    private async Task Download()
    {
        try
        {
            // In a real app, this would download the video file
            await Shell.Current.DisplayAlert("Info", "Download functionality will be implemented with actual video generation service", "OK");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error downloading video: {ex.Message}");
            await Shell.Current.DisplayAlert("Error", "Unable to download video", "OK");
        }
    }

    [RelayCommand]
    private async Task Back()
    {
        try
        {
            // Stop video before navigating back
            _mediaElement?.Stop();
            await Shell.Current.GoToAsync("..");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error navigating back: {ex.Message}");
        }
    }

    // Method to set video data from external source
    public void SetVideoData(string title, string source, string originalPrompt, string enhancedPrompt = "", string englishContent = "")
    {
        VideoTitle = title;
        VideoSource = source;
        OriginalPrompt = originalPrompt;
        EnhancedPrompt = enhancedPrompt;
        EnglishContent = englishContent;
        HasEnhancedPrompt = !string.IsNullOrEmpty(enhancedPrompt);
    }

    // IQueryAttributable implementation
    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        Debug.WriteLine("[VideoDisplay] ApplyQueryAttributes called");
        Debug.WriteLine($"[VideoDisplay] Received {query.Count} parameters:");
        
        // Start loading when receiving new video parameters
        IsLoading = true;
        
        foreach (var kvp in query)
        {
            Debug.WriteLine($"[VideoDisplay]   {kvp.Key}: {kvp.Value}");
        }

        if (query.ContainsKey("VideoTitle"))
        {
            VideoTitle = query["VideoTitle"].ToString() ?? "";
            Debug.WriteLine($"[VideoDisplay] Set VideoTitle: {VideoTitle}");
        }

        if (query.ContainsKey("VideoSource"))
        {
            var videoPath = query["VideoSource"].ToString() ?? "";
            Debug.WriteLine($"[VideoDisplay] Received VideoSource: {videoPath}");
            
            // Validate that the file exists
            if (File.Exists(videoPath))
            {
                VideoSource = videoPath;
                Debug.WriteLine($"[VideoDisplay] ✅ Video file exists, set VideoSource: {VideoSource}");
            }
            else
            {
                Debug.WriteLine($"[VideoDisplay] ❌ Video file not found: {videoPath}");
                // Fallback to sample video for demo
                VideoSource = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
                Debug.WriteLine($"[VideoDisplay] Using fallback video: {VideoSource}");
            }
        }
        else
        {
            Debug.WriteLine("[VideoDisplay] No VideoSource parameter received, using fallback");
            VideoSource = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
        }

        if (query.ContainsKey("OriginalPrompt"))
        {
            OriginalPrompt = query["OriginalPrompt"].ToString() ?? "";
            Debug.WriteLine($"[VideoDisplay] Set OriginalPrompt: {OriginalPrompt}");
        }

        if (query.ContainsKey("EnhancedPrompt"))
        {
            EnhancedPrompt = query["EnhancedPrompt"].ToString() ?? "";
            HasEnhancedPrompt = !string.IsNullOrEmpty(EnhancedPrompt);
            Debug.WriteLine($"[VideoDisplay] Set EnhancedPrompt: {EnhancedPrompt}");
            Debug.WriteLine($"[VideoDisplay] HasEnhancedPrompt: {HasEnhancedPrompt}");
        }

        if (query.ContainsKey("HasEnhancedPrompt"))
        {
            if (bool.TryParse(query["HasEnhancedPrompt"].ToString(), out bool hasEnhanced))
            {
                HasEnhancedPrompt = hasEnhanced;
                Debug.WriteLine($"[VideoDisplay] Set HasEnhancedPrompt from parameter: {HasEnhancedPrompt}");
            }
        }

        if (query.ContainsKey("EnglishContent"))
        {
            EnglishContent = query["EnglishContent"].ToString() ?? "";
            Debug.WriteLine($"[VideoDisplay] Set EnglishContent: {EnglishContent}");
        }

        VideoDescription = $"AI-generated viral video: {VideoTitle}";
        Debug.WriteLine($"[VideoDisplay] Set VideoDescription: {VideoDescription}");
        Debug.WriteLine("[VideoDisplay] ApplyQueryAttributes completed");
    }
} 
Con lo anterior, completamos el código de la aplicación.

Flujo completo de la aplicación

Como último paso, te comparto una imagen mostrando todo el proceso desde la entrada del prompt hasta la generación del video final:
Espero que el artículo te haya resultado entretenido.

¡Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada.