This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Monday, 05 January 2026
У якому ми створюємо голос, -) =-}Седина, що має значення ''t, простягає ключі до королівства AI
Тут мається на увазі, що ми маємо справу з інтерфейсами голосу: : * кожен хоче, щоб вони були ,}І ніхто їм не довіряє, але вони мають ., і вони мають право не на .)
Ви скажете, що 12 +MK5 і виштовхується LLM M" +МСК7, і йде до наступного поля перш ніж ви зможете виправити його . або гіршеМСК9 - це довір'я і автоМСК10submmує вашу форму з неправильною інформацією.
Час, коли ви дозволяєте LMM "control}МСК1' ваша форма тече}МСК2 Ви 'e ввели не-МСК4-чотирну скриньку в те, що повинно бути передбачуваним користувачем music .
Але що якщо б ми могли мати наш торт і з'їсти його теж =?}] вхідні для mouse, * LLM для перекладу }МСК2, але детермінований код для всього, що дійсно має значення, дорівнює ?}
Те, що ми сьогодні будуємо ''re -a "texTen Tepes episte " volume system, що використовує Thazor ServerSK5) local Whisper для peicesK}МС8МСК8'І локальна Ollama for leepSK9} Not unsective AI overspectalseM.)
Перш ніж ми пишемо будь-який код =,} нехай ''s встановить правила}МСК2'
Тут 'οs як це виглядає у практиці}МСК1'
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
Зверніть увагу на те, що LLM робить :} Вона перекладає .} Що ''s it}.} The state comping moints mopsK4} =00Options arepected .}Ця політика check}МСК6}Тут LLM-'des type of auto-'sconfirming or ask a user'se.'s
Нехай '} є чітким про об'єкт денного об'єкта :}
Ці значення є функціями ,} Не mugи .} Це робить систему рівноцінною}МСК2' testable}МСК3} і заслуговує на довір'я .'s
Ми маємо намір побудувати окремий "Пітер Blazor" app}МСК1) Тут ''s Над архітектурою :}
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
Architectecure нота # :} Жодна служба не залежить від іншого "ММСК0," що має значення "МСК1," що має значення "МСК2," що знає про "ац."
Першою має бути ,}Вам потрібен Wsper і Ollama, що працюють локально .} Додайте ці до вашого 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:
Після того, як ми піднімаємося вгору, ,} вичерпає модель Ollama}МСК1'
docker exec -it ollama ollama pull llama3.2:3b
Чому локальний катет Працювати все локально - це 'et про вартість -}МСК2's about testplution's ,'s neuctions ,} І дані }МСК5 Ваша трубка CI може продукувати ті ж контейнери, що й СМСК}Ва ваші тести впадають в ту ж кінцеву точку, що й кінцева точка .} No APITERC8}Немає мережевих даних, що залишають ваш комп'ютер .
Зауваження}МсісК0} The base.en Whisper модель швидкий і точний для English}.} Для production's ,} Розглянемо small.en або medium.en для кращої точності}МСК0}
Форми визначаються у JSO.} Це єдине джерело правди для того, що існують, і як вони поводяться :
{
"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
}
]
}
Моделі КМСК0, які представляють цю модель:
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);
Тому що схема - це декоративна схема. додавання полів ніколи не потребує торкання державного комп' ютераІ форма автоматично включає в себе це значення з контролем перебігу (S.)
Тут мається на увазі, що суть роботи є основною. :} The state magine doesn''}t використовує AIЛогіка, яка визначає перехід, що базується на чіткому правилі
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];
}
}
Деканові переходи є явною і пробною частиною: :}
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();
}
ci point =:} The _currentFieldIndex тільки просування на декламацію означає, що невдале або відмовлене підтвердження тримає вас на тому самому полі .' ї Користувач перебуває в контрольному .}
Зверніть увагу на логіку, що підтверджує логіку "МСК0," що означає "ММК1" - проста політика. alwaysConfirm =} автоМСК1ММСК2'Утожність або чутливе поле "МСК3" запитати користувача .
Dracor Ollama має одну роботу: }:} перетворити нерозбірливу людську мову в структуру поля значення }.}Тут ''s }МСК3'
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);
seps:} Екстрактор може запропонувати NeedsConfirmation: true}МСК0, але політика підтвердження завжди має кінцевий сказати:"ММСК1," "ЛМММСК2," опція є дорадницькою тенденцією
І в цій перспективі є КМСК0
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);
}
}
Чому температура ⇩0.1? сягає Ми хочемо, щоб LLM був нудною і рівноцінним до ., що означає те ж саме, що й кожного разу, коли ММСК2, тіло КМК3, применшує творчість, що й ми хочемо, щоб дані були однаковою величиною.
Штамп системи висвітлюється про LLM}МСК0's обмежену ролю =:}
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 видовжує , type format},} і виражає довіру }МСК2' Він також приймає рішення, що буде далі .'s
Сторона JavaScript вміщує мікрофон і перетворює його на "МмК0кГн моно ВАВМСК1"
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 };
})();
Чому конвертувати на WAV'S?} Whsper's missions personal personalize personalize semK0) but }.} No ,) always sembersion spective severalse canceled on the = tetags out of the server'seMSK5'} always se,) and the turnition is definatistic (same audio in msK8 tempsions out).}
The convertToWav function's seamples to 16}КМСХ1ІМСХ2'll keep you the WAV's bit-'twi сягаючи КМК4, but it's ''s in the Explicant's K00S}
UI показує поточне поле cele left },} З усіма полями, видимими на бічній панелі на правому боці =.} Бічна панель має значення довіри:} Користувачі можуть побачити, де вони знаходяться у формі ,} те, що вони мають ''ve вже відповів },} і що МСК4 йде далі }ММСК5'ІСНДСК6}
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
Тут ''s the tazor page cyrus}МісК1'
@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>
Диспетчер виправляє служби}МСК0," він має не має власної логіки розгалужуванняВін просто кличе служби в послідовності і передає результат з'єму МСК1
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);
}
}
Кожен крок є незалежним і проб'є #МСК0}Мерт ніколи не торкатись мережі.} LLM ніколи не торкнеться штату.
Темний режим реалізовано з нетиповими властивостями CSS}МсК0}
: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);
}
Перемикання теми відбувається за допомогою localStorage} :}
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 };
})();
Детермінатична архітектура йде в тесті ce.}ТутМСК1's a state templete perte for the Щасливий шлях #МСК2'
[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");
}
І тут мається на увазі, що негативне тести =-, а саме - впевненість у собі. сили Підтвердження незалежно від того, що LLM пропонує}МСК0}
[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);
}
І тест браузера PuppeerSharp, який фактично клацає річ:s
[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");
}
Повна програма для тестування працює на тестах, що працюють на сайті UI.}
Нехай '}] }Чому ми побудували його таким чином:
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
У традиційному підході все є LLM.) Кожне рішення - це norse-}МексиК2) Кожне випробування є пробабілістичними .}Кожен вада - "}Ті просто іноді робить те, що "-unproducible і unexpectablemK6}
У нашому підході}МСК0 , що LLM робить одна річ}:) Переводить мову до структурних значень }МСК1} Все інше є детерміналістичним C}МСК2, що ви можете зневаджувати , * testc4} І причина про .} Коли щось іде не так, ви можете вислизнути, чому саме .}
Це ДІМК0, і 3,, і 4) }МСК3 LLM перекладає }МСК4-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а-а."
docker compose -f devdeps-docker-compose.yml up -d
docker exec -it ollama ollama pull llama3.2:3b
cd Mostlylucid.VoiceForm
dotnet run
Перейти до http://localhost:5000
Натисніть " і почніть говорити !
Д-р Харріс: "Ті-д-д-д-д-д-д-д-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р
Ідеться про те, що вони є антиМСК3ІМСК4 підставити комп' ютер комп' ютер на належне місцеА не оракул, який робить рішення для нас.
Тепер йдіть побудувати щось там, де ви - сягнете не модель}ММСK1'Ідецид, що відбувається далі}МСК2'
У даний момент користувач читає запрошення і говорить далі, але що, якщо система може говорити назад}МЗК0) +ММСК1 додає text-'stemK3' ужить зворотний зв'язок , = Перетворює це на повну ІМСК5Інтерактивну голос) }МСК6ССССССК7
Архітектура вже підготовлена #:} Автомат sa випромінює події =,} Координати оркестратора сервіси +.}Тут вихідний голос - це просто ще одна послуга, яка слухає ці події
Наближається .}
Повнофункціональний код знаходиться у Mostlylucid.VoiceForm Проект у темі "МСК0"
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.