Determinististen äänimuotojen rakentaminen Blazorin ja paikallisten LLM:ien kanssa (Suomi (Finnish))

Determinististen äänimuotojen rakentaminen Blazorin ja paikallisten LLM:ien kanssa

Monday, 05 January 2026

//

16 minute read

Kymmenen käskyä

Johdanto

Jossa rakennamme äänijärjestelmän, joka ei anna avaimia tekoälyjen valtakunnalle.

Tässä on se juttu ääniyhteyksistä, jota kaikki haluavat. Kukaan ei luota niihin, ja he ovat oikeutettuja olemaan luottamatta niihin.

Suurin osa äänimuodoista epäonnistuu siinä vaiheessa, kun malli kuulee väärin päivämäärän tai päättää siirtyä helpolla eteenpäin MSK:n mukaan. Sanot, että MSK:n ja LLM:n otteista tulee virheellisiä tietoja, ennen kuin voit korjata sen.

Heti kun annat LLM:n ohjata MSK:tä, olet ottanut käyttöön muun kuin MSK:n ja MSK:n välisen mustan laatikon, jonka pitäisi olla ennustettavissa oleva käyttökokemus.

Mutta entä jos voisimme saada kakkumme ja syödä sen myös MSKK:n äänipanoksen mukavuuden vuoksi MSKK:n LLM:n kääntämiseen? deterministinen koodi Kaikelle, millä on oikeasti merkitystä

Se, mitä me tänään rakennamme MSKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK7KKKKKKKKKKKKKKKK8KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK

Puheenvapauden kymmenen käskyä

Ennen kuin kirjoitamme mitään koodia, anna meidän luoda säännöt.

  1. Puhe on häviöllinen syöte Se ei koskaan ole totuuden lähde. Se on vain yksi tapa kansoittaa kenttiä.
  2. LLM on kääntäjä Se poimii strukturoitua dataa sotkuisesta puheesta.
  3. Valtion kone omistaa muotovirran Määrittelevä koodi päättää, mikä kenttä on seuraava
  4. Vahvistus perustuu politiikkaan. Kynnysarvot ovat julkilausumia, ja ne pannaan täytäntöön lain mukaan, ei LLM-tuomiolla.
  5. Käyttäjä voi aina ohittaa Ääni on mukavuus, ei pakko.
  6. Kaikki kulkee paikallisesti Ei ulkoisia API-riippuvuuksia ydintoiminnoille
  7. Jokainen vuorovaikutus on kirjautunut Täydet kirjausketjut vianetsintään MSK:n vaatimustenmukaisuuteen ja uusintaan, kun asiat menevät pieleen
  8. Validointi on tyypin - mukainen Standardivalidaattorit, ei LLM-tulkinta
  9. Muodollinen skeema on deklaratiivinen JSON määrittelee muodon rakennelman, jonka koodi tulkitsee.
  10. Epäonnistuminen on sulavaa Kun puheentunnistus epäonnistuu, lomake toimii yhä

Tältä tämä näyttää käytännössä.

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

Huomaa, mitä LLM tekee MSKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK7KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK

Mitä tämä tekee?

Annetaan MSK:n tehdä selväksi, missä laajuudessa MSK:n toiminta on.

  • Ei luonnollista keskustelua Tämä on chatbotti yksi kenttä kerrallaan.
  • Ei adaptiivisia kysymyksiä LLM ei improvisoi jatkoa -:lle, .:lle.
  • Ei MSK:n avustajaa, ":n persoonaa LLM:n otteita arvostetaan. MSK:n otteita käytetään small talk -puheissa.
  • Ei kontekstia eri aloilla Jokainen poiminta on riippumatonta. LLM ei muista nimeäsi, kun pyydät sähköpostia.

Nämä rajoitteet ovat ominaisuuksia, ei vikoja, jotka tekevät järjestelmästä ennustettavissa olevan ja luotettavan.

Projektin rakenne

Me rakennamme itsenäisen Blazor Server -sovelluksen MSK:lle.

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

Arkkitehtiteos Yksikään palvelu ei ole riippuvainen toisesta MSKKKKKKKKKK4KKKKKKKK4KKKKKK4KKKKKK4KKKT tietää orkesterinjohtajasta Tämä tekee jokaisesta testattavasta testattavasta kappaleesta itsenäisen.

Infrastruktuurin perustaminen

Ensin on Whisper ja Ollama hoidettava paikallisesti . Lisää nämä devdeps-docker-compose.yml:

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:

Kun on pyörinyt, on vedetty malli Ollama :

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

Miksi paikallinen? Kaiken pyörittäminen paikallisesti kertoo kustannuksista, joita MSK:lla on, mutta ei API-nopeusrajoituksia, joita ei ole verkon vaihtelevuus, MSK:lla ei ole tietoja, jotka jättäisivät konettasi MSK:lle.

Huomautukset: Erytropoietiini base.en Whisper-malli on nopea ja tarkka englanninkieliselle MSKKKK:lle. small.en tai medium.en Parempaan tarkkuuteen

Delegoivan lomakkeen määritelmät

Lomakkeet on määritelty JSONMSK:ssa Tämä on yksi ainoa totuuden lähde sille, mitä kenttiä on olemassa ja miten ne käyttäytyvät.

{
  "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
    }
  ]
}

"Tätä mallia edustavat C:" -mallit

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

Koska skeema on deklaratiivinen. peltojen lisääminen ei koskaan vaadi valtionkoneen koskettamistaDrop a new field definition in the JSON, and the form including it with real flow control

Deterministisen muotovirran valtiollinen kone MSKKK

Tässä on ydinnäkemys. Valtion kone käyttää tekoälyä"MSKK" on puhtaasti C#" -logiikka, joka määrittää siirtymät selkeiden sääntöjen perusteella.

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

Valtion siirtymät ovat selkeitä ja testattuja.

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

Avainkohta Erytropoietiini _currentFieldIndex Tämä tarkoittaa epäonnistunutta tai hylättyä vahvistusta, joka pitää sinut samalla kentällä.

Huomaa vahvistuslogiikka, että se on yksinkertainen poliittinen tarkastus, ei LLM:n päättely, .:n päättely, suuri luottamus alwaysConfirm MSKKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM onMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM on on on on on on on on on on on on on on on on onMMMMMMMMMMMMMMMMMMMM onMMMMM on onMMMM on on onMMMM onMMMMMM onMMMMMMMMMMMMMMMM onMMMMM on on onMMMMM on on on on on on on onM onM on on on on onM M M M M M M M M M M M M M M M M M M M M M M M M M

Käännöksiä vain työpaikoille

Ollaman kaivajalla on yksi työpaikka, joka muuttaa sotkuisen ihmispuheen jäsennellyiksi kenttäarvoiksi.

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

"Tärkeää" Kaivaja voi ehdottaa NeedsConfirmation: trueMSKKKK2K:n lausunto on neuvoa-antava, mutta vahvistuspolitiikassa on aina lopullinen sanonta: MSKK2K:n lausunto on neuvoa-antava ja epävirallinen.

Ja täytäntöönpanosta MSKK:n

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

Miksi lämpötila on liian korkea? Haluamme, että LLM on tylsä ja ennustettavissa oleva, kun otetaan huomioon sama transkriptio, sen pitäisi ottaa sama arvo joka kerta, kun . Lämpötila on mahdollisimman pieni, se minimoi luovan vaihtelun, - € tarkalleen, mitä haluamme datan keräämiseksi . €

Järjestelmän kiireellisyys kertoo selkeästi LLMMSK:n rajoitetusta roolista.

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

LLM:n otteet validoivat tyyppiformaatin , ja raportoivat luottamuksesta Se ei päätä, mitä tapahtuu seuraavaksi .

Äänitallentimen WAV-selain

Javascript-sivu kuvaa mikrofonin äänen ja muuntaa sen 16kHz-mono-WAV:-ääneksi

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

Miksi käännytään WAVMSK:ksi? Whisper hyväksyy erilaisia formaatteja, mutta 16KHz mono WAV on optimaalinen -K2K2'S on juuri sitä, mitä malli on koulutettu .K4K:lle Ei transkoodausta yli palvelimella ,K6K ja muuntaminen on deterministinen (K7Same audio in =K9K9

Erytropoietiini convertToWav Funktion uudelleennäytteet 16KHzMK1I'Oll säästää sinut WAV:n otsikolta bit-K3KK4K, mutta se 'K:lta repo-.K:lta

Blazor UIMSK2-Cloumn Layout

Yhdyskunta näyttää nykyisen kentän näkyvästi vasemmalla, ja kaikki kentät näkyvät oikealla puolella olevassa sivupalkissa. Sivupalkilla on merkitystä luottamukselleKäyttäjät voivat nähdä, missä muodossa he ovat, mitä he ovat jo vastanneet ,:lle ja mitä ' on tulossa seuraavaksi .:lle Ei yllätyksiä .:lle

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

Tässä on Blazorin sivurakennelma.

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

Orkesterin orkesteri tuo kaiken yhteen

Orkesteri koordinoi palveluita. ei omaa haarautumislogiikkaaSe vain soittaa palveluja peräkkäin ja välittää tuloksia pitkin MSK:ta.

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

Jokainen askel on itsenäinen ja testattavissa valtion kone verkko ei koskaan koskeLLM:ää ei koske koskaan valtioon.

CSS-muuttujat voittoon

Pimeä tila toteutetaan CSS:n mukautetuilla ominaisuuksilla

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

Teema jatkuu paikallisen arkistoinnin kautta.

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

Yksikön testaustestit Selainintegraatio

Deterministinen arkkitehtuuri kannattaa testata valtion koneyksikön testi onnellista polkua varten.

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

Ja tässä on negatiivinen testi. MSK:n heikko itseluottamus. voimat Vahvistus riippumatta siitä, mitä LLM ehdottaa.

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

Ja PuppeteerSharp-selaintesti, joka oikeasti napsauttaa asioita

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

Koko testisarja testaa liiketoimintalogiikan , 22:n selaimen integraatiotestit UI.:n selaimen integraatiotestit.

Miksi tällä arkkitehtuurilla on merkitystä

Anna MSKK:n tutkia uudelleen, miksi rakensimme sen näin.

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

Perinteisessä lähestymistavassa: kaikki on LLM:ääJokainen päätös on epäolennainen ja selittämätön. Jokainen testi on todennäköinen. Jokainen vika on vain joskus niin, että "- ei ole toistettavissa ja selittämätön.

Lähestymistapamme mukaan LLM tekee yksi asiaKaikki muu on determinististä, että ,:n testiä ja perustelua .:lle Kun jokin menee pieleen, voit jäljittää, miksi .:n testiä voidaan käyttää väärin.

Tämä on käskyt MSKK1 ja 4 Toiminnossa : LLM kääntää , Valtion koneohjauksen virtauksen , päättää vahvistuksen .

Sen pyörittäminen itse

  1. Aloita riippuvuussuhteet
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
  1. Suorita sovellus MSKK
cd Mostlylucid.VoiceForm
dotnet run
  1. Navigoi http://localhost:5000

  2. Klikkaa asiakkaasi sisäänottoa ja aloita puhuminen.

Päätelmät

Ääniliittymien täytyy olla mustia laatikoita, kun puhe on vain yksi syötemenetelmä, kun puhutaan kääntäjistä ja pidetään virtauksen hallinta deterministisessä koodissa.

  • Ennustettavaa käytöstä Samat lähdöt
  • Testattava logiikka Yksikön kokeet yritysten sääntöjä varten
  • Tarkastuksen kohteena olevat päätökset Jokainen valtion siirtymä on kirjautunut
  • Armollinen rappeutuminen Ääni epäonnistuu ? Type sen sijaan
  • Paikallinen toiminta Ei pilviriippuvuuksia ydintoiminnoille

MSKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK tekoälyn laittaminen oikealle paikalleenVoimakas työkalu, joka kääntää ihmisen sotkuisuuden ja tietokoneen tarkkuuden välillä, ei oraakkeli, joka tekee päätöksiä puolestamme.

Mene nyt rakentamaan jotain, missä et päätä, mitä seuraavaksi tapahtuu.

Äänipalautteen saaminen osaksi (IVR)

Juuri nyt käyttäjä lukee kehotuksia ja puhuu vastauksia, mutta entä jos järjestelmä voisi puhu takaisinOsa MSK:ta lisää tekstiin --M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M4M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M)M)M)M3M)M3M)M3M3M3M)M3M3M3M)M3M3M3M3M3M3M)M3M3M3M3M3M3M3M3M3M3M3M3M3M)M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M)M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3M3

  • Lue ohjeet ääneen Ole hyvä ja sano koko nimesi.
  • Vahvista uutot Kuulin, että John Smith on oikeassa.
  • Äänitilavihjeet Sounds for recording start / stop , standing standing , statements
  • Vapaat kädet -operaatio Täydennä lomakkeet katsomatta ruutuun

Arkkitehtuuri on jo valmisteltu, valtiollinen kone lähettää tapahtumia, orkesteri koordinoi palveluita, ja äänituoton lisääminen on vain yksi palvelu, joka kuuntelee näitä tapahtumia.

Tulossa pian.

Lisää luettavaa

Koko lähdekoodi on Mostlylucid.VoiceForm Project in the RepoMSK

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.