# Construyendo formularios de voz determinantes con Blazor y LLM locales

## Los Diez Mandamientos

<datetime class="hidden">2026-01-05T14:30</datetime>

<!--category-- ASP.NET, Blazor, AI, LLM, Whisper, Ollama -->
## Introducción

**En la cual construimos una voz -to-forma sistema que doesns't mano las llaves al reino AI**

Aquí la cosa sobre las interfaces de voz : todo el mundo los quiere , nadie confía en ellos . Y ellos ' son correctos no a .

La mayoría de las formas de voz fallan el momento en que el modelo escucha mal una fecha o decide "helvilly" saltar adelante. Usted dice "Mayo 12th 1984" y los extractos LLM "Mayo 2014" y avanza al campo siguiente antes de que usted pueda corregirlo. O peor: alucina confianza y auto-envia su forma con datos incorrectos.

El momento en que dejas una LLM "control" tu forma fluir, you've introdujo una caja negra non-determinista en lo que debe ser una experiencia de usuario predecible.

Pero lo que si pudiéramos tener nuestro pastel y comerlo demasiado ? Entrada de voz para la comodidad , LLM para la traducción , pero *código determinístico* para todo lo que realmente importa

Que ' es lo que nosotros 're edificio hoy en día-a "Diez mandamientos conformes" sistema de forma de voz utilizando Blazor Server, local Whisper para el discurso-to-text, y local Ollama para la extracción del campo. Ninguna dependencia de la nube. No impredecible AI superseñores. Arquitectura limpia.

[TOC]

## Los diez mandamientos de las formas habilitadas de voz

Antes de escribir cualquier código , let's establece las reglas :

1. **El habla es una entrada perdida** - It's nunca la fuente de la verdad, apenas una manera de poblar los campos
2. **LLM es traductor** - Extrae datos estructurados del discurso desordenado, nada más
3. **La máquina estatal posee flujo de forma** - Código determinista decide qué campo es el siguiente
4. **La confirmación es política.** - Los umbrales son declarativos y aplicados por código, no LLM sentencia
5. **El usuario siempre puede anular** - Voz es conveniencia, no compulsión
6. **Todo funciona localmente.** - Ninguna dependencia externa de API para la funcionalidad del núcleo
7. **Cada interacción está registrada** - Ruta de auditoría completa para la depuración , cumplimiento, y reproducir cuando las cosas van mal
8. **La validación es type-based** - Validadores estándar, no interpretación LLM
9. **El esquema de formulario es declarativo** - JSON define la estructura de la forma , código lo interpreta
10. **El fracaso es elegante.** - Cuando el reconocimiento del habla falla, la forma todavía funciona

Aquí 's cómo se ve esto en la práctica :

```mermaid
flowchart LR
    A[User Speaks] --> B[Whisper STT]
    B --> C[Raw Transcript]
    C --> D[Ollama LLM]
    D --> E[Extracted Value]
    E --> F{Validation}
    F -->|Pass| G{Policy Check}
    F -->|Fail| H[Show Error]
    G -->|High Confidence| I[Auto-Confirm]
    G -->|Low Confidence| J[Ask User]
    J --> K{User Confirms?}
    K -->|Yes| L[State Machine: Next Field]
    K -->|No| M[Retry Recording]
    I --> L
    H --> M

    style D stroke:#f96,stroke-width:2px
    style F stroke:#6f6,stroke-width:2px
    style G stroke:#ff9,stroke-width:2px
```

Note lo que el LLM hace : se traduce . Que's it. La máquina del estado decide fluir. Validación decide corrección. La política de chequeo-no el LLM-decida si auto-confirmar o preguntar al usuario.

## Lo que esto hace no hacer

Let's ser claro sobre el alcance

- **No hay conversación natural** - Esto es 't un chatbot. Un campo a la vez.
- **Sin cuestionamiento adaptativo** - El esquema de la forma es estático. El LLM hace 't improvisa seguir-ups.
- **No "assistant" personalidad** - La LLM extrae los valores , hace la charla pequeña.
- **Ningún contexto en todos los campos** - Cada extracción es independiente. La LLM doesns't recuerda tu nombre cuando pides correo electrónico.

Estas restricciones son características , no bugs. Hacen el sistema predecible, testable, y confiable.

## Estructura del proyecto

Nosotros 're la construcción de un Blazor servidor independiente app. Here's la arquitectura:

```
Mostlylucid.VoiceForm/
├── Config/
│   └── VoiceFormConfig.cs         # Configuration binding
├── Models/
│   ├── FormSchema/                # Form definitions
│   ├── State/                     # Session state
│   └── Extraction/                # LLM input/output
├── Services/
│   ├── Stt/                       # Speech-to-text (Whisper)
│   ├── Extraction/                # Field extraction (Ollama)
│   ├── Validation/                # Type-based validators
│   ├── StateMachine/              # Form flow control
│   └── Orchestration/             # Coordinates everything
├── Components/
│   └── Pages/                     # Blazor UI
└── wwwroot/js/
    └── audio-recorder.js          # Web Audio API capture
```

**Nota arquitectónica** Ningún servicio depende de otro " hacia abajo." El extractor doesns't sabe sobre el estado machine. El validador does't sabe sobre el orquestador. Esto hace cada pieza testable independientemente .

## Creación de la infraestructura

Primero , you' necesitará Whisper y Ollama corriendo localmente. Añadir estos a su `devdeps-docker-compose.yml`:

```yaml
services:
  whisper:
    image: onerahmet/openai-whisper-asr-webservice:latest
    container_name: whisper-stt
    ports:
      - "9000:9000"
    environment:
      - ASR_MODEL=base.en
      - ASR_ENGINE=faster_whisper
    volumes:
      - whisper-models:/root/.cache/huggingface
    restart: unless-stopped

  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama-models:/root/.ollama
    restart: unless-stopped

volumes:
  whisper-models:
  ollama-models:
```

Después de girar , tirar del modelo para Ollama:

```bash
docker exec -it ollama ollama pull llama3.2:3b
```

> **¿Por qué local?** Ejecutar todo localmente es 't sobre costo-it's sobre testabilidad, determinism, y localidad de datos. Tu tubería CI puede girar encima de los mismos contenedores. Tus pruebas golpean los mismos extremos. No API rate limits, no network variabilitation, no data lefting your machine.

**Nota** Los `base.en` Susurro modelo es rápido y preciso para Inglés . Para la producción , considerar `small.en` o `medium.en` para una mejor precisión

## La forma Schema: Definiciones declarativas de la forma

Las formas se definen en JSON. Esta es la única fuente de verdad para qué campos existen y cómo se comportan

```json
{
  "id": "customer-intake",
  "name": "Customer Intake Form",
  "fields": [
    {
      "id": "fullName",
      "label": "Full Name",
      "type": "Text",
      "prompt": "Please say your full name.",
      "required": true
    },
    {
      "id": "dateOfBirth",
      "label": "Date of Birth",
      "type": "Date",
      "prompt": "What is your date of birth?",
      "required": true,
      "confirmationPolicy": {
        "alwaysConfirm": true
      }
    },
    {
      "id": "email",
      "label": "Email Address",
      "type": "Email",
      "prompt": "What is your email address?",
      "required": true
    },
    {
      "id": "phone",
      "label": "Phone Number",
      "type": "Phone",
      "prompt": "What is your phone number?",
      "required": false
    },
    {
      "id": "notes",
      "label": "Additional Notes",
      "type": "Text",
      "prompt": "Any additional notes?",
      "required": false
    }
  ]
}
```

Los modelos C# que representan esto

```csharp
public record FormDefinition(
    string Id,
    string Name,
    List<FieldDefinition> Fields);

public record FieldDefinition(
    string Id,
    string Label,
    FieldType Type,
    string Prompt,
    bool Required = true,
    ConfirmationPolicy? ConfirmationPolicy = null);

public enum FieldType
{
    Text,
    Date,
    Email,
    Phone,
    Choice  // Constrained vocabulary - especially important for voice
}

public record ConfirmationPolicy(
    bool AlwaysConfirm = false,
    double ConfidenceThreshold = 0.85);
```

Porque el esquema es declarativo **agregar campos nunca requiere tocar la máquina de estado**. Dejar caer una nueva definición de campo en el JSON, y la forma la incluye automáticamente con el control de flujo adecuado

## La máquina estatal : Flujo de forma determinista

Aquí 's el núcleo perspicacia: **la máquina estatal no usa AI**. It's lógica pura C# que determina transiciones basadas en reglas explícitas.

```csharp
public class FormStateMachine : IFormStateMachine
{
    private FormSession _session = null!;
    private int _currentFieldIndex;

    public FormSession StartSession(FormDefinition form)
    {
        _session = new FormSession
        {
            Id = Guid.NewGuid().ToString(),
            Form = form,
            Status = FormStatus.InProgress,
            StartedAt = DateTime.UtcNow,
            FieldStates = form.Fields.ToDictionary(
                f => f.Id,
                f => new FieldState { FieldId = f.Id, Status = FieldStatus.Pending })
        };

        // First field starts in progress
        if (form.Fields.Count > 0)
        {
            _session.FieldStates[form.Fields[0].Id].Status = FieldStatus.InProgress;
        }

        return _session;
    }

    public FieldDefinition? GetCurrentField()
    {
        if (_currentFieldIndex >= _session.Form.Fields.Count)
            return null;

        return _session.Form.Fields[_currentFieldIndex];
    }
}
```

Las transiciones del estado son explícitas y testables

```csharp
public StateTransitionResult ProcessExtraction(
    ExtractionResponse extraction,
    ValidationResult validation)
{
    var currentField = GetCurrentField();
    if (currentField == null)
        return new StateTransitionResult(false, "No current field");

    var fieldState = _session.FieldStates[currentField.Id];
    fieldState.AttemptCount++;

    // Validation failed? Stay on current field
    if (!validation.IsValid)
    {
        return new StateTransitionResult(
            false,
            $"Validation failed: {validation.ErrorMessage}");
    }

    // Store the pending value
    fieldState.PendingValue = extraction.Value;
    fieldState.PendingConfidence = extraction.Confidence;

    // Check confirmation policy - this is rules, not AI
    var policy = currentField.ConfirmationPolicy
        ?? new ConfirmationPolicy();

    var needsConfirmation = policy.AlwaysConfirm
        || extraction.Confidence < policy.ConfidenceThreshold;

    if (needsConfirmation)
    {
        fieldState.Status = FieldStatus.AwaitingConfirmation;
        return new StateTransitionResult(
            true,
            "Please confirm this value",
            RequiresConfirmation: true);
    }

    // Auto-confirm high confidence values
    return ConfirmValue();
}
```

**Punto clave** Los `_currentFieldIndex` sólo los avances sobre la confirmación , nunca sobre la extracción . Esto significa una extracción fallida o la confirmación rechazada te mantiene en el mismo campo . El usuario permanece en control .

Note la lógica de confirmación : it's un simple control de la política , no LLM razonamiento. Alta confianza + no `alwaysConfirm` = auto-confirmar. Bajo campo de confianza o sensible = preguntar al usuario.

## La traducción LLM's Job: solamente

El extractor Ollama tiene un trabajo : convertir el discurso humano desordenado en valores estructurados del campo . Here's el contrato :

```csharp
public interface IFieldExtractor
{
    Task<ExtractionResponse> ExtractAsync(
        ExtractionContext context,
        CancellationToken ct = default);
}

public record ExtractionContext(
    FieldDefinition Field,
    string Prompt,
    string Transcript);

public record ExtractionResponse(
    string FieldId,
    string? Value,
    double Confidence,
    bool NeedsConfirmation,  // Suggestion only - policy has final say
    string? Reason);
```

**Importante** El extractor puede sugerir `NeedsConfirmation: true`, pero la política de confirmación siempre tiene la última palabra . La opinión de LLM' es advisory, no autoritative.

Y la implementación :

```csharp
public class OllamaFieldExtractor : IFieldExtractor
{
    private readonly HttpClient _httpClient;
    private readonly string _model;

    public async Task<ExtractionResponse> ExtractAsync(
        ExtractionContext context,
        CancellationToken ct = default)
    {
        var systemPrompt = BuildSystemPrompt(context.Field);
        var userPrompt = $"User said: \"{context.Transcript}\"";

        var request = new
        {
            model = _model,
            messages = new[]
            {
                new { role = "system", content = systemPrompt },
                new { role = "user", content = userPrompt }
            },
            format = "json",
            stream = false,
            options = new { temperature = 0.1 }  // Low = deterministic
        };

        var response = await _httpClient.PostAsJsonAsync(
            "/api/chat", request, ct);

        return ParseResponse(response, context.Field.Id);
    }
}
```

**¿Por qué la temperatura 0.1?** Queremos que el LLM sea aburrido y predecible Dado la misma transcripción , debe extraer el mismo valor cada vez que la temperatura 0.1 minimiza la variación creativa -Exactamente lo que queremos para la extracción de datos .

El sistema prompt es explícito sobre el LLM's papel limitado:

```csharp
private string BuildSystemPrompt(FieldDefinition field)
{
    return $"""
        You are a data extraction assistant. Extract the {field.Label}
        from the user's speech.

        Field type: {field.Type}

        Return JSON only:
        {{
          "fieldId": "{field.Id}",
          "value": "<extracted value or null>",
          "confidence": <0.0-1.0>,
          "needsConfirmation": <true/false>,
          "reason": "<brief explanation>"
        }}

        Rules:
        - For dates, output ISO format (YYYY-MM-DD)
        - For emails, output lowercase
        - For phones, output digits only
        - If you can't extract, set value to null
        - Be conservative with confidence scores

        DO NOT:
        - Ask follow-up questions
        - Suggest next steps
        - Make assumptions beyond the transcript
        """;
}
```

Los extractos LLM valida el formato tipo , y los informes confianza . No decide lo que sucede siguiente .

## El Navegador de audio a WAV

El lado JavaScript captura el audio del micrófono y lo convierte a 16kHz mono WAV:

```javascript
window.voiceFormAudio = (function () {
    let mediaRecorder = null;
    let audioChunks = [];
    let audioContext = null;
    let dotNetRef = null;

    async function startRecording() {
        const stream = await navigator.mediaDevices
            .getUserMedia({ audio: true });

        audioContext = new AudioContext();
        audioChunks = [];

        const options = { mimeType: 'audio/webm;codecs=opus' };
        mediaRecorder = new MediaRecorder(stream, options);

        mediaRecorder.ondataavailable = (event) => {
            if (event.data.size > 0) {
                audioChunks.push(event.data);
            }
        };

        mediaRecorder.onstop = async () => {
            stream.getTracks().forEach(track => track.stop());

            const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
            const wavBlob = await convertToWav(audioBlob);
            const wavBytes = await wavBlob.arrayBuffer();

            // Send to Blazor
            const uint8Array = new Uint8Array(wavBytes);
            await dotNetRef.invokeMethodAsync(
                'OnRecordingComplete',
                Array.from(uint8Array));
        };

        mediaRecorder.start(100);
    }

    return { initialize, startRecording, stopRecording };
})();
```

**Por qué convertir a WAV?** Whisper acepta varios formatos , pero 16kHz mono WAV es óptimo-it's exactamente lo que el modelo fue entrenado en. No transcodificación por encima del servidor , resultados consistentes, y la conversión es determinista (same audio en = mismos bytes out).

Los `convertToWav` la función remuestra a 16kHz-I' le ahorrará la cabecera WAV bit-twiddling, pero sí 's en el repo.

## El diseño Blazor UI: Two-Column

La interfaz de usuario muestra el campo actual prominentemente a la izquierda con todos los campos visibles en una barra lateral a la derecha **La barra lateral es importante para la confianza**: los usuarios pueden ver donde están en la forma, lo que ellos've ya respondió, y qué's viene next. No hay sorpresas.

```mermaid
graph LR
    subgraph "Left Panel"
        A[Current Field Prompt]
        B[Record Button]
        C[Transcript Display]
        D[Confirmation Dialog]
    end

    subgraph "Right Sidebar"
        E[Field 1: Full Name ✓]
        F[Field 2: DOB - Active]
        G[Field 3: Email - Pending]
        H[Field 4: Phone - Pending]
    end

    style F stroke:#36f,stroke-width:2px
    style E stroke:#6f6,stroke-width:2px
```

Aquí 's la estructura de la página Blazor:

```razor
@page "/voiceform/{FormId}"
@inject IFormOrchestrator Orchestrator
@rendermode InteractiveServer

<div class="top-bar">
    <h1>Voice Form</h1>
    <button class="theme-toggle" onclick="voiceFormTheme.toggle()">
        Dark Mode
    </button>
</div>

<main>
    <div class="voice-form-layout">
        <!-- Left: Active Field -->
        <div class="active-field-panel">
            @if (_currentField != null)
            {
                <div class="current-prompt">
                    <h2>@_currentField.Label</h2>
                    <p class="prompt-text">@_message</p>
                </div>

                <AudioRecorder OnAudioCaptured="HandleAudioCaptured"
                               IsRecording="_isRecording"
                               IsProcessing="_isProcessing" />

                @if (!string.IsNullOrEmpty(_transcript))
                {
                    <TranscriptDisplay Transcript="_transcript"
                                       Confidence="_transcriptConfidence" />
                }

                @if (_showConfirmation)
                {
                    <ConfirmationDialog ExtractedValue="_pendingValue"
                                        OnConfirm="HandleConfirm"
                                        OnReject="HandleReject" />
                }
            }
        </div>

        <!-- Right: Form Overview -->
        <div class="form-sidebar">
            <h3>@_session.Form.Name</h3>
            <div class="form-fields-list">
                @foreach (var field in _session.Form.Fields)
                {
                    var state = _session.GetFieldState(field.Id);
                    <div class="form-field-item @GetFieldClass(field, state)">
                        <div class="field-status-icon">
                            @GetStatusIcon(state.Status)
                        </div>
                        <div class="field-info">
                            <div class="field-name">@field.Label</div>
                            <div class="field-value">
                                @(state.Value ?? "Waiting")
                            </div>
                        </div>
                    </div>
                }
            </div>
        </div>
    </div>
</main>
```

## El Orchestrator, trayéndolo todo.

El orquestador coordina los servicios **ninguna lógica de ramificación propia**- sólo llama servicios en secuencia y pasa los resultados a lo largo de :

```csharp
public class FormOrchestrator : IFormOrchestrator
{
    private readonly ISttService _sttService;
    private readonly IFieldExtractor _extractor;
    private readonly IFormValidator _validator;
    private readonly IFormStateMachine _stateMachine;
    private readonly IFormEventLog _eventLog;

    public async Task<ProcessingResult> ProcessAudioAsync(byte[] audioData)
    {
        var currentField = _stateMachine.GetCurrentField();
        if (currentField == null)
            return ProcessingResult.Error("No current field");

        // Step 1: Speech to text
        var sttResult = await _sttService.TranscribeAsync(audioData);
        await _eventLog.LogAsync(new TranscriptReceivedEvent(
            currentField.Id, sttResult.Transcript, sttResult.Confidence));

        // Step 2: Extract structured value
        var context = new ExtractionContext(
            currentField, currentField.Prompt, sttResult.Transcript);
        var extraction = await _extractor.ExtractAsync(context);
        await _eventLog.LogAsync(new ExtractionAttemptEvent(
            currentField.Id, extraction));

        // Step 3: Validate
        var validation = _validator.Validate(currentField, extraction.Value);

        // Step 4: State machine decides next step
        var transition = _stateMachine.ProcessExtraction(extraction, validation);

        return new ProcessingResult(
            Success: transition.Success,
            Message: transition.Message,
            Session: _stateMachine.GetSession(),
            RequiresConfirmation: transition.RequiresConfirmation,
            PendingValue: extraction.Value);
    }
}
```

Cada paso es independiente y testable . La máquina del estado **nunca toca la red**. El LLM **nunca toca el estado**.

## Modo oscuro : Variables CSS para el triunfo

Modo oscuro se implementa con CSS propiedades personalizadas:

```css
:root {
    --bg-primary: #f8fafc;
    --bg-secondary: #ffffff;
    --text-primary: #1e293b;
    --accent-blue: #3b82f6;
    --accent-green: #22c55e;
}

[data-theme="dark"] {
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --text-primary: #f1f5f9;
    --accent-blue: #60a5fa;
    --accent-green: #4ade80;
}

body {
    background: var(--bg-primary);
    color: var(--text-primary);
}
```

El tema alterna persiste vía localStorage:

```javascript
window.voiceFormTheme = (function () {
    const THEME_KEY = 'voiceform-theme';

    function toggle() {
        const current = document.documentElement
            .getAttribute('data-theme') || 'light';
        const newTheme = current === 'dark' ? 'light' : 'dark';
        document.documentElement.setAttribute('data-theme', newTheme);
        localStorage.setItem(THEME_KEY, newTheme);
    }

    // Initialize from saved preference or system preference
    function init() {
        const saved = localStorage.getItem(THEME_KEY);
        const systemDark = window.matchMedia(
            '(prefers-color-scheme: dark)').matches;
        const theme = saved || (systemDark ? 'dark' : 'light');
        document.documentElement.setAttribute('data-theme', theme);
    }

    return { toggle, init };
})();
```

## Pruebas : Pruebas de unidad + Integración del navegador

La arquitectura determinista paga en testing. Here's una unidad de la máquina de estado prueba para la trayectoria feliz:

```csharp
[Fact]
public void ProcessExtraction_HighConfidence_AutoConfirms()
{
    // Arrange
    var form = CreateTestForm();
    var stateMachine = new FormStateMachine();
    stateMachine.StartSession(form);

    var extraction = new ExtractionResponse(
        FieldId: "fullName",
        Value: "John Smith",
        Confidence: 0.95,  // High confidence
        NeedsConfirmation: false,
        Reason: "Clear speech");

    var validation = ValidationResult.Success();

    // Act
    var result = stateMachine.ProcessExtraction(extraction, validation);

    // Assert
    result.Success.Should().BeTrue();
    result.RequiresConfirmation.Should().BeFalse();

    var fieldState = stateMachine.GetSession()
        .GetFieldState("fullName");
    fieldState.Status.Should().Be(FieldStatus.Confirmed);
    fieldState.Value.Should().Be("John Smith");
}
```

Y aquí 's una prueba negativa - baja confianza **fuerzas armadas** confirmación sin importar lo que el LLM sugiera

```csharp
[Fact]
public void ProcessExtraction_LowConfidence_RequiresConfirmation()
{
    // Arrange
    var form = CreateTestForm();
    var stateMachine = new FormStateMachine();
    stateMachine.StartSession(form);

    var extraction = new ExtractionResponse(
        FieldId: "fullName",
        Value: "John Smith",
        Confidence: 0.65,  // Below threshold
        NeedsConfirmation: false,  // LLM says no, but policy overrides
        Reason: "Noisy audio");

    var validation = ValidationResult.Success();

    // Act
    var result = stateMachine.ProcessExtraction(extraction, validation);

    // Assert
    result.RequiresConfirmation.Should().BeTrue(
        "Policy threshold (0.85) should override LLM suggestion");

    var fieldState = stateMachine.GetSession()
        .GetFieldState("fullName");
    fieldState.Status.Should().Be(FieldStatus.AwaitingConfirmation);
}
```

Y una prueba de navegador TippeteerSharp que realmente hace clic en things:

```csharp
[Fact]
public async Task HomePage_ThemeToggle_ShouldSwitchTheme()
{
    await _page!.GoToAsync(BaseUrl);
    await _page.WaitForSelectorAsync(".theme-toggle");

    var initialTheme = await _page.EvaluateFunctionAsync<string?>(
        "() => document.documentElement.getAttribute('data-theme')");

    // Click toggle
    await _page.ClickAsync(".theme-toggle");
    await Task.Delay(100);

    var newTheme = await _page.EvaluateFunctionAsync<string?>(
        "() => document.documentElement.getAttribute('data-theme')");

    newTheme.Should().NotBe(initialTheme);
}

[Fact]
public async Task VoiceFormPage_Sidebar_ShouldShowAllFields()
{
    await _page!.GoToAsync($"{BaseUrl}/voiceform/customer-intake");
    await _page.WaitForSelectorAsync(".form-fields-list");

    var fieldItems = await _page.QuerySelectorAllAsync(".form-field-item");

    fieldItems.Should().HaveCount(5, "Customer intake form has 5 fields");
}
```

La suite de prueba completa se ejecuta 98 pruebas-76 pruebas de unidad para la lógica de negocio, 22 pruebas de integración del navegador para la UI.

## Por qué es importante esta arquitectura

Let's revisitar por qué lo construimos de esta manera

```mermaid
graph TD
    subgraph "Traditional Voice Form"
        A1[User Speaks] --> B1[LLM]
        B1 --> C1[LLM decides field]
        C1 --> D1[LLM validates]
        D1 --> E1[LLM confirms]
        E1 --> F1[LLM advances form]
    end

    subgraph "Ten Commandments Approach"
        A2[User Speaks] --> B2[Whisper STT]
        B2 --> C2[Ollama Extract]
        C2 --> D2[C# Validator]
        D2 --> E2[Policy Check]
        E2 --> F2[State Machine]
    end

    style B1 stroke:#f96,stroke-width:2px
    style C1 stroke:#f96,stroke-width:2px
    style D1 stroke:#f96,stroke-width:2px
    style E1 stroke:#f96,stroke-width:2px
    style F1 stroke:#f96,stroke-width:2px

    style C2 stroke:#f96,stroke-width:2px
    style D2 stroke:#6f6,stroke-width:2px
    style E2 stroke:#6f6,stroke-width:2px
    style F2 stroke:#6f6,stroke-width:2px
```

En el enfoque tradicional **todo es el LLM**. Cada decisión es non-deterministic. Cada prueba es probabilística. Cada error es "a veces sólo hace eso"-inreproducible e inexplicable.

En nuestro enfoque , el LLM hace **Una cosa**: traducir discurso a valores estructurados. Todo lo demás es determinista C# que usted puede debug, test, y razón sobre. Cuando algo va mal, usted puede trazar exactamente por qué.

Esto es mandamientos 2, 3, y 4 en acción: LLM traduce , estado máquina controles flujo, política decide confirmación.

## Ejecutarlo tú mismo

1. Iniciar las dependencias:

```bash
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
```

2. Ejecutar la aplicación

```bash
cd Mostlylucid.VoiceForm
dotnet run
```

3. Navega hasta `http://localhost:5000`

4. Haga clic en "Customer Intake" y empezar a hablar

## Conclusión

Interfaces de voz don't tienen que ser cajas negras. Tratando el habla como simplemente otro método de entrada , LLMs como traductores, y manteniendo el control de flujo en código determinista, obtienes :

- **Comportamiento previsible** - Mismas entradas, mismas salidas
- **Lógica probable** - Pruebas de unidad para las reglas de negocio
- **Decisiones que pueden ser objeto de auditoría** - Cada transición de estado está registrada
- **Degradación agraciada** - Voz falla? Tipo en su lugar
- **Operación local** - No hay dependencias en la nube para la funcionalidad del núcleo

Los "Ten Mandamientos" son't sobre ser anti-AI. Ellos're sobre **poner la IA en su lugar apropiado**: una herramienta poderosa que se traduce entre la confusión humana y la precisión de la computadora, no un oráculo que toma decisiones para nosotros.

Ahora vaya a construir algo donde usted -no el modelo -decide lo que sucede next.

## Viene en parte 2: Realimentación de voz (IVR)

Ahora mismo , el usuario lee prompts y habla responses. Pero lo que si el sistema podría *Devuélveme la palabra.*? Parte 2 añadirá texto-to-speech feedback, convirtiendo esto en un IVR completo (Interactive Voice Response) system:

- **Leer avisos en voz alta** Por favor, diga su nombre completo
- **Confirmar las extracciones** - "Escuché 'John Smith'. Es eso correcto?"
- **Señales de estado de audio** - Sonidos para grabar errores start/stop, confirmation,
- **Operación sin manos** - Completar formularios sin mirar a la pantalla

La arquitectura ya está preparada  : la máquina estatal emite eventos  , las coordenadas orquestadoras servicios  . Añadir salida de voz es apenas otro servicio que escucha esos eventos  .

*# Pronto # # . # # Pronto # # MSK0 # # Pronto # # Pronto # # MSK0 # # Pronto # #*

## Lectura adicional

- [Uso de Docker Compose para Dependencias de Desarrollo](/blog/dockercomposedevdeps) - Creación de servicios locales
- [HTMX con ASP.NET Core](/blog/htmxwithaspnetcore) - Server-driven UI patterns
- [El DiSE Cooker: Dioses no confiables](/blog/blog-article-cooking-dise-part3-untrustworthy-gods) - Por qué los LLMs necesitan verificación

El código fuente completo está en el `Mostlylucid.VoiceForm` proyecto en el repo.