Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
163629 stories
·
33 followers

Why bloc.add(...) Isn't What You Think: Synchronous Event Fast-Paths and 0ms Form Bindings

1 Share

The Muscle-Memory Illusion

"An asynchronous network stream belongs at the perimeter of your architecture; once an AI agent's UI lands on the client, every keystroke, validation rule, and leaf rebuild must execute in 0ms on a synchronous signal graph."
β€” Randal L. Schwartz

When Flutter developers inspected the code samples from our bloc_signals_genui 1.0 announcement, their classic package:bloc muscle memory sparked an immediate question:

// Mutate bound form fields synchronously in 0ms
surfaceBloc.add(
  const UpdateFormField(
    path: '/user/name',
    value: 'Randal L. Schwartz',
  ),
);

The question went something like this: Wait. It is called a Bloc and you are calling .add(event). Doesn't that push the event into an asynchronous StreamController sink, schedule a microtask on the Dart event loop, sever the call stack, and update the UI in a later frame?

In BlocSignal, .add() dispatches events synchronously on the active call stack. When an event handler and its transformer execute synchronously, the entire dispatch-to-emission pipeline runs inline on the exact same call stack in 0ms. By the time the line immediately following surfaceBloc.add(...) executes:

  1. The underlying a2ui_core DataModel signal cell is updated.
  2. Every GenericBinder signal binding and CheckRule validation expression on the surface has recomputed.
  3. surfaceBloc.value holds the new SurfaceReady immutable state snapshot.

Understanding this difference requires examining the mechanics of event propagation in Dart and the reasons client applications need a synchronous core.

  • The Asynchronous Microtask Tax in Classic BLoC: Classic package:bloc routes events through Dart StreamController pipes. Even when an event handler does zero asynchronous work, Dart's stream architecture defers execution across an asynchronous microtask boundary, delaying state propagation by at least one event loop turn, severing call stacks, and inducing input lag during rapid keystrokes.
  • The FutureOr Fast-Path in BlocSignal: By inspecting handler return types dynamically (final result = onEvent(event); if (result is Future) ...), BlocSignal executes synchronous event handlers immediately inline on the caller's call stack with zero microtask allocation while maintaining full async support for network tasks.
  • Why Generative UI Demands a Synchronous Core: Google's official package:a2ui_core uses preact_signals for its DataModel and two-way form bindings. Pairing an asynchronous network perimeter (restartable() SSE streaming) with a synchronous event core (UpdateFormField) enables 120 FPS form interactions without frame tearing or flaky test waits.

Anatomy of the Microtask Tax: Why Classic bloc.add() Was Always Asynchronous

To see why developers assume .add() must be asynchronous, look at how classic package:bloc routes events under the hood:

flowchart TD
    UI["Widget Tap / Keystroke\nbloc.add(Event)"]
    Sink["_eventController.sink.add(event)\n(StreamController.broadcast)"]
    Loop["Dart Event Loop\n(Scheduled Microtask Hop)"]
    Transformer["EventTransformer / StreamSubscription\n(stream.asyncExpand)"]
    Handler["on<Event>((event, emit) {...})\n(Handler Invocation)"]
    State["_stateController.add(newState)\n(Second Microtask Hop)"]
    UIRebuild["StreamBuilder / BlocBuilder Rebuild\n(Later Frame)"]

    UI --> Sink
    Sink -->|"Microtask Hop #1"| Loop
    Loop --> Transformer
    Transformer --> Handler
    Handler --> State
    State -->|"Microtask Hop #2"| UIRebuild

In package:bloc, every Bloc manages an internal StreamController<Event>. Calling add(event) pushes the event into this stream sink.

In Dart's dart:async library, adding an event to a StreamController defers listener execution by scheduling a microtask on the Dart event loop. The Dart runtime continues executing whatever synchronous code remains in the current call stack, and processes stream events only after that synchronous block yields control.

Even if your event handler contains zero await calls and returns immediately:

// Classic package:bloc
on<UpdateFormField>((event, emit) {
  emit(state.copyWith(name: event.value));
});

The execution flow still incurs two asynchronous microtask hops: one to deliver the event to the handler, and a second to deliver the newly emitted state through the state stream to widget builders.

This architecture introduces three concrete penalties in production applications:

1. Stale State on the Next Line

Because the microtask has not fired yet, reading bloc.state on the line immediately following bloc.add(...) returns the old state:

// Classic package:bloc
bloc.add(const UpdateFormField(path: '/user/name', value: 'Randal'));

// Stale! The microtask is still waiting in the event loop queue:
print(bloc.state.name); // Prints previous value ('Alice'), retaining the stale state

Developers working around this behavior must introduce asynchronous delays (await Future<void>.delayed(Duration.zero)) or await pump() in tests just to observe the effect of a synchronous assignment.

2. Severed Call Stacks in Debugging

When an exception occurs inside a classic BLoC event handler, or when a developer pauses on a breakpoint, the debug stack trace reveals only the stream infrastructure:

#0  MyBloc._onUpdateFormField (my_bloc.dart:42)
#1  _MapStream._handleData (dart:async/stream_pipe.dart:214)
#2  _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341)
#3  _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271)
#4  _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774)
#5  _StreamController._add (dart:async/stream_controller.dart:648)
#6  _StreamController.add (dart:async/stream_controller.dart:596)
#7  _microtaskLoop (dart:async/schedule_microtask.dart:40)
#8  _startMicrotaskLoop (dart:async/schedule_microtask.dart:49)

The original call siteβ€”the TextField.onChanged callback, the button tap, or the form submit handlerβ€”has been pruned from memory by the microtask queue. The causal link between user gesture and state transition is lost.

3. Frame Tearing and Input Lag in Interactive Forms

Modern mobile and desktop displays refresh at 60 Hz (16.6ms per frame) or 120 Hz (8.3ms per frame). When a user types rapidly on a software keyboard, the operating system dispatches input events in rapid succession.

If every keystroke schedules a microtask hop through a stream controller, the framework delays processing until after the current frame's layout phase has begun. In complex generative forms with cross-field validation rules, this microtask latency creates a race condition where the native text input field and the bloc's emitted state fall out of phase across frames. The result is visual cursor jitter, character transposition, and dropped inputs.

Inside the Synchronous Event Fast-Path

BlocSignal retains the classic BLoC public API (add, on<E>, emit, state) while eliminating StreamController and microtask queues from event routing.

Here is the exact dispatch mechanism in BlocSignalMixin:

void add(Event event) {
  if (isClosed) return;

  final currentObserver = BlocSignalObserver.observer;
  if (currentObserver != null) {
    try {
      currentObserver.onEvent(this, event);
    } on Exception catch (e, stackTrace) {
      onError(e, stackTrace);
    }
  }

  runZoned(
    () {
      try {
        final result = onEvent(event);
        if (result is Future) {
          unawaited(_handleAsyncResult(result, event));
        } else {
          _notifyEventCompleted(event);
        }
      } on Object catch (e, stackTrace) {
        onError(e, stackTrace);
        _notifyEventCompleted(event);
        if (e is Error) rethrow;
      }
    },
    zoneValues: {
      zoneEventKey: event,
      zoneBlocKey: this,
    },
  );
}

Notice the decisive branching check on line 17:

final result = onEvent(event);
if (result is Future) {
  unawaited(_handleAsyncResult(result, event));
} else {
  _notifyEventCompleted(event);
}

In BlocSignal, on<E> registers handlers that return Dart's FutureOr<void>.

When a handler contains synchronous logic, it returns plain void (null). The result is Future check evaluates to false. The engine executes the handler inline on the caller's stack frame, notifies completion, and finishes synchronously. Zero microtask scheduling occurs.

Here is the corresponding path through onEvent and the registered handlers:

@mustCallSuper
FutureOr<void> onEvent(Event event) {
  final matched = _handlers.where((h) => h.isType(event));
  if (_handlers.isNotEmpty && matched.isEmpty) {
    throw StateError(
      'add(${event.runtimeType}) was called without a registered event handler.',
    );
  }
  List<Future<dynamic>>? futures;
  for (final registry in matched) {
    final result = registry.handler(event, emit) as dynamic;
    if (result is Future) {
      (futures ??= []).add(result);
    }
  }
  if (futures != null) {
    return Future.wait(futures).then<void>((_) {});
  }
}

If none of the matched handlers return a Future, futures remains null. The method returns null synchronously.

sequenceDiagram
    autonumber
    participant UI as Flutter Widget / Caller
    participant Bloc as BlocSignal.add(event)
    participant Mixin as onEvent / Handler
    participant Signal as Signal<State> (_state.value)
    participant Observer as GenericBinder / Subscriber

    UI->>Bloc: add(UpdateFormField(...))
    activate Bloc
    Bloc->>Mixin: onEvent(event)
    activate Mixin
    Mixin->>Mixin: handler(event, emit)
    Mixin->>Signal: _state.value = newState
    activate Signal
    Signal->>Observer: Synchronous Push/Pull Invalidation
    deactivate Signal
    Mixin-->>Bloc: return null (Synchronous)
    deactivate Mixin
    Bloc-->>UI: return void (Exact Same Call Stack)
    deactivate Bloc
    Note over UI: Next line executes immediately in 0ms!

The Async Wrapper Trap (Repository Scar)

Maintaining this synchronous fast-path requires strict discipline in concurrency transformers.

In an early development phase of BlocSignal, higher-order event transformer wrappers were written using async syntax for convenience:

// THE PATHOGEN: Marking transformer closures async unconditionally
EventTransformer<E, S> faultyTransformer<E, S>() {
  return (event, handler, emit) async {
    // ...
    await handler(event, emit);
    // ...
  };
}

In Dart, marking a function async forces the runtime to allocate a Future and schedule execution across a microtask turn upon the first await, even if the wrapped handler is completely synchronous. In concurrency policies like droppable() or restartable(), this microtask gap delayed clearing reentrancy flags (isProcessing = false), causing back-to-back synchronous events emitted in the exact same frame to be dropped.

The permanent repository invariant codified in our architecture skill specifies:

The Synchronous Fast-Path Rule: In event transformers, never mark outer or inner forwarding closures async. Inspect the handler's return type dynamically: final result = handler(event, emit); if (result is Future) { return result.whenComplete(...); }. Execute synchronous completions immediately inline so reentrancy flags clear in the exact same call stack.

Intact Call Stacks

Because synchronous handlers execute inline, setting a breakpoint inside an event handler preserves the entire execution context:

#0  A2uiSurfaceBloc._onUpdateFormField (a2ui_surface_bloc.dart:630)
#1  BlocSignalMixin.onEvent (bloc_signal_mixin.dart:257)
#2  BlocSignalMixin.add.<anonymous closure> (bloc_signal_mixin.dart:99)
#3  _rootRun (dart:async/zone.dart:1399)
#4  _CustomZone.run (dart:async/zone.dart:1301)
#5  runZoned (dart:async/zone.dart:1805)
#6  BlocSignalMixin.add (bloc_signal_mixin.dart:96)
#7  A2uiTextField.onChanged (a2ui_text_field.dart:118)
#8  EditableTextState._formatAndSetValue (editable_text.dart:2890)

The stack trace connects the widget's onChanged event directly to the state machine's internal handler.

Why Generative UI Demands a Synchronous Core

This synchronous fast-path is essential when building Generative UI with Google's official A2UI (Agent-to-User Interface) protocol.

In A2UI, an LLM streams a declarative UI description over the network using Server-Sent Events (SSE). The JSON payload contains surface definitions, component hierarchies, dynamic data models, and client-side validation rules (CheckRule).

Under the hood, Google's official package:a2ui_core uses preact_signals for its reactive primitives:

  • DataModel stores state in reactive signal nodes addressed by JSON Pointer paths (such as /user/name or /order/total).
  • GenericBinder provides two-way WritableBinding connections between UI inputs and the data model.
  • CheckRule evaluates client-side validation logic (for example regular expressions, required field checks, or numeric ranges) synchronously as signal expressions.

If you wrap a2ui_core in a traditional stream-based state manager, you sever this reactive signal graph. Every form field change must leave the signal graph, enter a stream sink, pass through a microtask queue, re-emerge into a stream handler, mutate the signal, and schedule another microtask to notify widget builders.

In bloc_signals_genui, A2uiSurfaceBloc handles form updates with zero microtask latency:

void _onUpdateFormField(
  UpdateFormField event,
  void Function(A2uiSurfaceState) emit,
) {
  final surfaceId = event.surfaceId ?? _activeSurfaceId;
  if (surfaceId == null) return;

  final surface = _processor.model.getSurface(surfaceId);
  if (surface == null) return;

  // Synchronous data model mutation with signal batching
  batch(() {
    surface.dataModel.set(event.path, event.value);
  });
  _touchSurface(surfaceId);
  _bumpSurfaceVersion(surfaceId);

  _emitSurfaceSnapshot(emit);
}

When a user edits an input field in an A2UI surface:

  1. The widget dispatches surfaceBloc.add(UpdateFormField(path: '/email', value: input)).
  2. _onUpdateFormField executes inline on the exact same thread.
  3. batch(() { surface.dataModel.set(...); }) updates the underlying a2ui_core signal graph atomically.
  4. CheckRule expressions bound to /email re-evaluate synchronously. If the input is valid, dependent signal properties update in the exact same frame.
  5. _emitSurfaceSnapshot(emit) calls emit(SurfaceReady(...)), which writes directly to _state.value.
  6. Any submit button whose enabled state depends on CheckRule.isValid reflects the new state on the very next render frame with zero microtask delays.

Proving It in Code: The Zero-Await Assertion and 1-Pump Widget Test

The most direct way to observe the synchronous event fast-path is through unit and widget tests.

In classic BLoC, testing an event dispatch requires asynchronous coordination:

// Classic package:bloc test: requires async stream waiting
test('classic bloc requires stream awaiting', () async {
  final bloc = MyClassicBloc();
  bloc.add(const Increment());

  // Fails synchronously! bloc.state is still initial:
  // expect(bloc.state, equals(1)); 

  // Must await the stream or pump microtasks:
  await expectLater(bloc.stream, emits(1));
});

In BlocSignal, because the event executes inline, testing is strictly synchronous:

// BlocSignal test: 100% synchronous assertions
test('supports on<E> registration and handles events synchronously', () {
  final bloc = RegistryBloc();
  expect(bloc.value, equals(0));

  bloc.add(Increment());
  // Immediately true on the very next line of execution:
  expect(bloc.value, equals(1));

  bloc.add(Decrement());
  expect(bloc.value, equals(0));
});

Single-Pump Widget Tests

This predictability extends directly to Flutter widget testing. When testing complex A2UI generative surfaces with dynamic validation rules, testing with bloc_signals_genui_flutter requires exactly one pump:

testWidgets(
  'enables submit button synchronously when form fields pass CheckRule validation',
  (tester) async {
    final bloc = A2uiSurfaceBloc()..add(ProcessJsonMessage(signupFormSurfaceJson));
    await tester.pumpWidget(MaterialApp(home: A2uiSurfaceView(bloc: bloc)));

    // Initially, the submit button is disabled by its CheckRule:
    final initialBtn = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
    expect(initialBtn.onPressed, isNull);

    // Dispatch synchronous form field update:
    bloc.add(
      const UpdateFormField(
        path: '/email',
        value: 'randal@example.com',
      ),
    );

    // Exactly one pump to allow Flutter to paint the new frame:
    await tester.pump();

    // The submit button is enabled immediately:
    final enabledBtn = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
    expect(enabledBtn.onPressed, isNotNull);
  },
);

You never need tester.pumpAndSettle(). You never need arbitrary timeouts. You pump one frame, and the UI reflects the updated state.

The Architectural Division: Async at the Perimeter, Sync in the Core

BlocSignal embraces asynchronous operations where system physics dictates them: network I/O, database access, background isolates, and large language model streams are inherently asynchronous operations.

The core architectural principle is the division of responsibilities:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             ASYNCHRONOUS PERIMETER                     β”‚
β”‚  β€’ LLM Streaming SSE / WebSocket Firehose              β”‚
β”‚  β€’ REST API Requests & Platform Channels               β”‚
β”‚  β€’ Isolated with restartable() / droppable() Mutexes   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό (IngestStream / ProcessMessages)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              SYNCHRONOUS STATE CORE                    β”‚
β”‚  β€’ BlocSignal & A2uiSurfaceBloc State Machine          β”‚
β”‚  β€’ 0ms Inline on<E> Event Dispatch                     β”‚
β”‚  β€’ Atomic Two-Way Form Bindings (batch)                β”‚
β”‚  β€’ Topological Signal Graph (a2ui_core DataModel)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό (UI = Ζ’(State))
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             SYNCHRONOUS PRESENTATION                   β”‚
β”‚  β€’ Flutter 120 FPS Rendering Pipeline (build / paint)  β”‚
β”‚  β€’ ANSI Terminal TUI (examples/genui_tui_agent)        β”‚
β”‚  β€’ Zero Microtask Lag, Zero Frame Tearing              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. Quarantine Asynchrony at the Perimeter: Use BlocSignal with concurrency transformers (restartable(), droppable()) to absorb incoming network streams, handle user cancellation, and sequence external requests.
  2. Execute State Transitions Synchronously in the Core: Once data crosses the perimeter into client memory, execute state transitions inline on the caller's call stack. Form inputs, validation rules, navigation state, and derived calculations belong in a synchronous signal graph.
  3. Project UI Synchronously Downstream: Flutter's rendering pipeline (build $\rightarrow$ layout $\rightarrow$ paint) is 100% synchronous. Feeding it synchronous state snapshots ensures zero-frame loading flickers, intact debugging call stacks, and 120 FPS interface responsiveness.

The next time you call bloc.add(...) in BlocSignal, remember: it is a direct, synchronous state machine executing on your call stack in 0ms.

Explore the code and packages:

Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

C# + Ollama: Give Your App a Voice with Phonon

1 Share

⚠ This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the πŸ€– in C# are 100% mine.

Hi!

Apple Silicon users, please try this C# sample and let us know whether speech generation and playback work on your Mac!

ElBruno.Speech 1.6.0-preview.5 keeps setup simple: call services.AddOllamaSpeech() for Microsoft.Extensions.AI.ITextToSpeechClient. It defaults to local Ollama and the phonon model. Let’s turn a sentence into a WAV file, with local inference handled by Ollama.

Conceptual flow, not a recording of generated audio. Image generated with AI.

1. Get ready

Use .NET 8 or 10 and a supported Apple Silicon Mac running Ollama with MLX. Ollama’s speech announcement recommends version 0.40.11 or later.

ollama pull phonon
dotnet new console -n HelloPhonon -f net10.0
cd HelloPhonon
dotnet add package ElBruno.Speech.Tts.Ollama --version 1.6.0-preview.5
dotnet add package ElBruno.Speech.Audio --version 1.6.0-preview.5
dotnet add package Microsoft.Extensions.DependencyInjection --version 10.0.13

For .NET 8, replace net10.0 with net8.0. Make sure Ollama is running.

Windows friends: the C# client can call a supported remote Mac, but local Ollama speech currently requires MLX. Windows/Linux server support is pending upstream, with no promised date.

2. Replace Program.cs

The MEAI speech API is experimental. The pragma below explicitly opts into it for this preview.

#pragma warning disable MEAI001
using ElBruno.Speech;
using ElBruno.Speech.Audio;
using ElBruno.Speech.Tts.Ollama;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddOllamaSpeech();
using var provider = services.BuildServiceProvider();
var speech = provider.GetRequiredService<ITextToSpeechClient>();
using var pcm = new MemoryStream();
await foreach (var update in speech.GetStreamingAudioAsync(
"Hello from C sharp! My app has a voice thanks to Ollama Phonon.",
new TextToSpeechOptions { AudioFormat = "pcm" }))
{
foreach (var audio in update.Contents.OfType<DataContent>())
await pcm.WriteAsync(audio.Data);
}
byte[] samples = pcm.ToArray();
if (samples.Length == 0 || samples.Length % 2 != 0)
throw new InvalidDataException("Expected complete PCM16 audio samples.");
WavWriter.Write("hello.wav",
new AudioFormat(24000, 1, AudioSampleFormat.Int16), samples);
Console.WriteLine($"Saved: {Path.GetFullPath("hello.wav")}");

Run it:

dotnet run

On a supported Mac, listen with:

afplay hello.wav

The client receives PCM chunks incrementally. This short example buffers them, then writes a WAV with the final sizes: 24 kHz, mono, signed 16-bit PCM. It does not play audio while streaming.

No configuration is required for local Ollama: the defaults are http://localhost:11434/, model phonon, the model’s default voice, and WAV output. This example requests PCM per call for streaming. Using a remote Mac? Replace the registration with services.AddOllamaSpeech(new OllamaSpeechOptions { Endpoint = new Uri("http://YOUR-MAC:11434/") });. The library resolves the speech route automatically. Keep that server on a trusted network; this example does not configure authentication or expose it.

3. Try the complete demo

The full C# sample also saves complete WAV and streamed PCM/WAV files, validates their format, rejects silent output, and supports optional playback.

Honest status: 49 synthetic provider/sample test cases pass on each of .NET 8 and .NET 10, including no-options registration, default request values, and URL normalization for complete and streaming requests. My Windows host could not load Phonon because MLX was unavailable, so I have not generated or listened to real Phonon speech yet. Automated checks are not proof of pronunciation or sound quality.

Community testing invitation, not proof of working audio. Image generated with AI.

If you test it, share your Mac chip, macOS and Ollama versions, whether hello.wav plays, and how it sounds in issue #106. Please include any error message, but no secrets or private audio.

NuGet package | Release notes | Ollama speech docs


Happy coding!

Greetings

El Bruno

More posts in my blog ElBruno.com.

More info in https://beacons.ai/elbruno




Read the whole story
alvinashcraft
8 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 11.0 RC 3

1 Share

You can build .NET 11.0 RC 3 from the repository by cloning the release tag v11.0.100-rc.3.26522.148 and following the build instructions in the main README.md.

Alternatively, you can build from the sources attached to this release directly.
More information on this process can be found in the dotnet/dotnet repository.

Attached are PGP signatures for the GitHub generated tarball and zipball. You can find the public key at https://dot.net/release-key-2023

Read the whole story
alvinashcraft
9 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 10.0.14 / 10.0.403

1 Share

You can build .NET 10.0 from the repository by cloning the release tag v10.0.403 and following the build instructions in the main README.md.

Alternatively, you can build from the sources attached to this release directly.
More information on this process can be found in the dotnet/dotnet repository.

Attached are PGP signatures for the GitHub generated tarball and zipball. You can find the public key at https://dot.net/release-key-2023

Read the whole story
alvinashcraft
9 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 8.0.33 / 8.0.133

1 Share

You can build .NET 8.0 from the repository by cloning the release tag v8.0.133 and following the build instructions in the main README.md.

Alternatively, you can build from the sources attached to this release directly.
More information on this process can be found in the dotnet/dotnet repository.

Attached are PGP signatures for the GitHub generated tarball and zipball. You can find the public key at https://dot.net/release-key-2023

Read the whole story
alvinashcraft
9 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 9.0.22 / 9.0.123

1 Share

You can build .NET 9.0 from the repository by cloning the release tag v9.0.123 and following the build instructions in the main README.md.

Alternatively, you can build from the sources attached to this release directly.
More information on this process can be found in the dotnet/dotnet repository.

Attached are PGP signatures for the GitHub generated tarball and zipball. You can find the public key at https://dot.net/release-key-2023

Read the whole story
alvinashcraft
9 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories