# Costruire moduli di voce deterministica con Blazor e LLM locali

## La Via dei Dieci Comandamenti

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

<!--category-- ASP.NET, Blazor, AI, LLM, Whisper, Ollama -->
## Introduzione

**In cui si costruisce un sistema vocale [55] - [56] to [56] - [56] form system che doesn [56] ' [56] hand the keys to the AI Kingdom**

In questo caso, la cosa relativa alle interfacce vocali è che tutti li vogliono, nessuno si fida di loro e loro non si fidano di loro.

La maggior parte delle forme vocali falliscono nel momento in cui il modello mishears una data o decide di "Helpfully "Helpfully skip available .Hai detto che "Maggio MRSK4Maggio MRSK5M e gli estratti LLM "Maggio MRSK7Maggio MRSK7M e avanza al campo successivo prima di poterlo correggere .M o peggio :M ha allucinazioni di fiducia e auto-MsK10Invia la tua forma con dati errati .M

Nel momento in cui si lascia un LLM di controllo LLM" di controllo " di flusso di forma , di flusso ha introdotto una scatola nera non - di Deterministic in quello che dovrebbe essere un prevedibile user experience . di esperienza

Ma cosa succederebbe se potessimo avere la nostra torta e mangiarla troppo? *Codice deterministico* per tutto ciò che effettivamente conta

Che il sistema di forma vocale di T' è conforme ai comandamenti di T" utilizzando il server Blazor [Blazor], [Whisper] locale per il discorso - di T- [Text] di T, [Text] e Ollama locale per l'estrazione di campo . No cloud dipendency . No imprevedibile AI overlords . Just clean architecture .

[TOC]

## I Dieci Comandamenti della Voce

Prima di scrivere un qualsiasi codice [49], [49] lascia che [54]' [56] stabilisce le regole [56]: [56]

1. **Il discorso è ingresso lossy** Non si tratta mai della fonte della verità, ma solo di un modo per popolare i campi.
2. **LLM è un traduttore** Estrae dati strutturati dal discorso disordinato e nient'altro
3. **La macchina statale possiede il flusso di forma** Il codice deterministico decide il campo successivo
4. **La conferma è basata sulla politica [55]- [56]** Le soglie sono dichiarative ed eseguite con il codice [49], [54] non con la sentenza LLM
5. **L'utente può sempre annullare** La voce è convenienza  .  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6. **Tutto funziona localmente** Nessuna dipendenza esterna dalle API per le funzionalità di base
7. **Ogni interazione è registrata** Sentiero di audit completo per il debugging della conformità della sostanza in esame e per il replay quando le cose vanno storte
8. **La convalida è basata sul tipo [55]- [56]** Validatori standard non interpretativi LLM
9. **Lo schema della forma è dichiarativo** JSON definisce la struttura della forma. Il codice , è interpretato da JSON.
10. **Il fallimento è aggraziato** Quando il riconoscimento vocale fallisce, il modulo funziona ancora.

In questo caso, l'' è il modo in cui questo appare nella pratica [49]: [56]

```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
```

Notate cosa fa il LLM [47]: [56] traduce [47]. [56] che [56]' [56] che [57] la macchina statale decide il flusso [54]. [56] che la convalida decide la correttezza [54]. [56] il controllo della politica non è il LLM [56]- [56] decide se auto- [56]- [56]confermare o chiedere all'utente [56]. [56]

## Che cosa fa questo Doesn

Lasciare che i ' siano chiari sull'ambito di applicazione

- **Nessuna conversazione naturale** Questo è un chatbot, un campo alla volta, un campo alla volta, un chatbot, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat, un chat
- **Nessun interrogatorio adattivo** Lo schema di forma è statico... .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
- **Nessuna personalità assistante [47]** I valori degli estratti di LLM sono i seguenti:
- **Nessun contesto tra i campi** Ogni estrazione è indipendente... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Questi vincoli sono caratteristiche che non sono bug, ma che rendono il sistema prevedibile e verificabile.

## Struttura del progetto

We &gt; ' &gt; Costruire un Blazor Server app &gt; . &gt; Qui &gt; ' &gt; L'architettura &gt; : &gt;

```
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 architettonica** Nessun servizio dipende da un altro S"Scegliere verso il basso S."Scheda L'estrattore doesn S'Sheet conosce la macchina statale SH.Sheet Il validatore doesn'Sheet conosce l'orchestratore SH.Sheet Questo rende ogni pezzo testabile indipendentemente SH.S

## Configurazione dell'infrastruttura

Prima di tutto Whisper e Ollama che funzionano localmente Aggiungi questi al tuo `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:
```

Dopo la filatura verso l'alto, tirare un modello per Ollama

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

> **Perché locale [50]? [50]** Eseguire tutto ciò che localmente isn®'® circa il costo®-®it®'® circa la provabilità®, determinismo®, e data località®. La vostra pipeline CI può far girare gli stessi contenitori®. I vostri test hanno colpito gli stessi endpoint.Nessun limite di velocità API®,′ nessuna variabilità della rete®, nessun dato che lasci la vostra macchina®.

**Nota [47]: [47]** La `base.en` Il modello Whisper è veloce e preciso per l'inglese. `small.en` oppure `medium.en` per una migliore precisione [55]. [56]

## Il modulo di definizione del formulario SCHEMA:.0.0.2 del modulo di dichiarazione

I moduli sono definiti in JSON®.® Questa è l'unica fonte di verità per quali campi esistono e come si comportano.

```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
    }
  ]
}
```

I modelli C.P.R.M.S.K.O. che rappresentano questo C.P.R.M.S.M.K.O.

```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);
```

Poiché lo schema è dichiarativo **aggiungere campi non richiede mai di toccare la macchina di stato**Il modulo include automaticamente una nuova definizione di campo nel campo JSON, e il modulo lo include con un corretto controllo del flusso.

## Il flusso di forma deterministica della macchina statale [47]:

In questo caso, la valutazione di base è stata effettuata da [47]' [47] **la macchina di stato doesn®'® usa AI**La logica pura di C.P.R.M.M.S.K.2 che determina le transizioni basate su regole esplicite

```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];
    }
}
```

Le transizioni di stato sono esplicite e testabili

```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 chiave [50]: [50]** La `_currentFieldIndex` Solo avanzamenti alla conferma [49], [56] mai all'estrazione [56]. [56] Ciò significa un'estrazione non riuscita o una conferma respinta ti mantiene sullo stesso campo [56]. [56] L'utente rimane nel controllo [56]. [56]

Si noti che la logica di conferma [47]: [47] it [47] ha un semplice controllo politico [47], [56] non ha un ragionamento LLM [47] Elevata fiducia [56] + [56] no `alwaysConfirm` Auto-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

## La LLM/'/Job/:/Job Translation Only

L'estrattore di Ollama ha un lavoro [47] : [47] trasformano discorso umano disordinato in valori di campo strutturati [47] . [47] Qui ' [47] ha contratto [47] : [47]

```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 [55]: [56]** L'estrattore può suggerire `NeedsConfirmation: true`Il parere di LLM®' è consultivo [47] non autorevole.

E l'attuazione [55]: [56]

```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);
    }
}
```

**Perché la temperatura di 0.1?** Vogliamo che l'LLM sia noioso e prevedibile, dato lo stesso trascrizione... .., .. .. ..dovrebbe estrarre lo stesso valore ogni volta che la temperatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Il prompt del sistema è esplicito in merito al ruolo limitato di LLM/' [47]

```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
        """;
}
```

L'LLM estrae [47], [56] convalida il formato di tipo [56], [56] e segnala la fiducia [54]. [56] Esso doesn [56]' [56]t decide cosa succede dopo [56]. [56]

## Il browser Audio Registratore [47]:.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.

Il lato JavaScript cattura l'audio del microfono e lo converte in WAV®16SHz mono WAV®:S

```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 };
})();
```

**Perché convertire in WAV** Whisper accetta i vari formati [49], [47] ma il mono WAV è ottimale [54]- [54]it [54] è esattamente ciò che il modello è stato addestrato su [54]. [54] Nessuna transcodifica aerea sul server [49] risultati coerenti [56], [56] e la conversione è deterministica [54]( [56] lo stesso audio in [49] = [56] lo stesso bytes out [49]

La `convertToWav` ricampioni di funzione a S16SHZ-S'SystemsK2Systemsll vi risparmia l'header WAV bitS-Systemstwiddling S,S, ma sS'Ss nel S.S repoSMSK6S

## La disposizione di Blazor UI/:/Two/-/Column

L'interfaccia utente mostra il campo corrente prominente sulla sinistra con tutti i campi visibili in una barra laterale sulla destra **La barra laterale conta per la fiducia**Gli utilizzatori possono vedere dove si trovano nella forma [55], [56] ciò che hanno già risposto [56]' [56] e ciò che [56]' [56] che arriva dopo [56] Nessuna sorpresa [56]

```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
```

In questo caso, la struttura della pagina Blazor è la "Blazor" ("Blazor page structure") ("Blazor page structure") ("Blazor page structure") ("Blazor page structure") ("Blazor page structure") ("Blazor page structure").

```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>
```

## L'orchestratore : ha portato tutto insieme

L'orchestratore coordina i servizi **nessuna logica ramificante propria**Il servizio di chiamata di appena di chiamata di S- in sequenza e passa i risultati lungo il S: SRT project

```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);
    }
}
```

Ogni passaggio è indipendente e testabile. **non tocca mai la rete**L'LLM **non tocca mai lo stato**.

## Modalità scura CSS Variabili CSS per la Vittoria

La modalità scura è implementata con le proprietà personalizzate CSS

```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);
}
```

Il tema toggle persiste tramite localStorage [47]: [47]

```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 };
})();
```

## Verifica dell'integrazione del browser dei test dell'unità di S:

L'architettura deterministica si ripaga nel testare il test [47]. [47] Qui [47]' [47] è un test per unità di macchina statale per il percorso felice [56]: [56]

```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");
}
```

E in questo caso il test negativo è il test del test del test del test del test del test del test del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del controllo del veicolo. **forze** conferma indipendentemente da ciò che la LLM suggerisce

```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);
}
```

E un test del browser PuppeteerSharp che fa clic sulle cose

```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 di test completa esegue i test delle unità di test di M98 per la logica aziendale , per i test di integrazione del browser di 22 per l'interfaccia utente. per i test di integrazione del browser

## Perché questa architettura è importante

Lasciate che gli ' possano rivisitare il motivo per cui l'abbiamo costruito in questo modo.

```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
```

Nell'approccio tradizionale [55], [56] **tutto è il LLM**Ogni insetto è un insetto che a volte fa che l'"- non sia riproducibile ed inspiegabile.

Nel nostro approccio [49], [49] la LLM fa **una cosa**Tutto il resto è deterministico C.I.M.M.M.K.I.M.K.I.M.K.I.M.K.I.M.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.K.I.J.K.I.K.I.I.K.I.K.I.K.K.I.K.I.K.I.K.I.K.K.I.K.I.K.K.K.I.K.K.I.K.K.I.K.K.K.I.K.K.I.K.K.K.K.I.K.K.K.K.K.K.I.K.K.K.K.K.K.K.K.K.K.K.J.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.

Questo è Comandi S2,S S3,S e S4S in azione S:S LLM traduce S,S stato SMSK4S controllo del flusso delle macchine S,S la politica decide conferma S.S

## Eseguire te stesso

1. Avviare le dipendenze [55]: [56]

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

2. Eseguire l'app S:S

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

3. Vai a `http://localhost:5000`

4. Clicca su Clicca su Clicca su Client Intake e inizia a parlare Clicca su Client Intake

## Conclusione

Interfacce vocali don®'®T devono essere scatole nere®.® Trattando il discorso come solo un altro metodo di input®,® LLMs come traduttori e mantenendo il controllo del flusso nel codice deterministico®,® si ottiene [49]:®

- **Comportamento prevedibile** Stessi input (ingressi) (in uscita) (in uscita) (in uscita) (in entrata) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) uscita) (in uscita) uscita) (in uscita) (in uscita) (in uscita) uscita) uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (in uscita) (
- **Logica testabile** Test unitari per le norme commerciali
- **Decisioni soggette a revisione** Ogni stato di transizione è registrato
- **Degradazione graziosa** Il tipo di voce non riesce invece a raggiungere il tipo di voce
- **Funzionamento locale** Nessuna dipendenza dal cloud per le funzionalità di base

I Comandamenti di Ten di S" sono di S' di SMSK2 di essere antiS- di S. di SMSK4 di S' di SMSK5 di circa **mettere l'IA al suo posto**Un potente strumento che si traduce tra la confusione umana e la precisione del computer non è un oracolo che prende decisioni per noi

Ora vai a costruire qualcosa in cui si [55]- [56]non il modello [56]- [56]decide cosa succede dopo [56]. [56]

## In arrivo nella parte di Feedback della voce di S( di S) di SMSK2

In questo momento, l'utente legge i prompt e parla le risposte ma se il sistema potesse *Rispondi.*La parte del testo aggiungerà il testo - a - "Speech feedback" , trasformandolo in una risposta vocale interattiva completa "IVR" ( "Interactive Voice Response" ) "System" "System" : "Speech Response"

- **Leggi i suggerimenti ad alta voce** Per favore, dica il suo nome completo.
- **Confermate le estrazioni** Si è sentito parlare di 'John Smith '. è che corretto ?"
- **Scrivi uno stato audio** Suoni per la registrazione dell'inizio della registrazione degli errori di conferma del sistema di registrazione del sistema di registrazione del sistema di registrazione del sistema di registrazione del sistema di registrazione del sistema di registrazione del sistema
- **Funzionamento senza mani [47]-** Completa le forme senza guardare lo schermo

L'architettura è già preparata... ..:... ..la macchina di stato emette eventi... ..,... ..l'orchestratrice coordina i servizi... ..L'aggiunta dell'uscita vocale è solo un altro servizio che ascolta quegli eventi... .. .....

*In arrivo*

## Ulteriore lettura

- [Uso di Docker Compose per le dipendenze di sviluppo](/blog/dockercomposedevdeps) Configurazione dei servizi locali
- [TMX con nucleo ASP/./N2SNET](/blog/htmxwithaspnetcore) Modelli di UI guidati da Server S-
- [Gli Dei Inaffidabili di DiSE Cooker](/blog/blog-article-cooking-dise-part3-untrustworthy-gods) Perché gli LLM hanno bisogno di verifica

Il codice sorgente completo è nel `Mostlylucid.VoiceForm` Progetto nell'ambito del piano di ristrutturazione per la ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della ristrutturazione della