From 8123c8ae51198b2c467acf21a515ac9ef432830e Mon Sep 17 00:00:00 2001 From: gabewillen Date: Fri, 10 Jul 2026 18:02:22 -0500 Subject: [PATCH 1/5] refactor: reshape speech recognizer into transcriber engine with injected components - rename speech/recognizer -> speech/transcriber (STT; 'detector' stays reserved for future VAD-class components) across code, tests, tools, bench ids, and domain-boundary checks - dissolve speech/recognizer_routes: the transcriber is now a non-templated engine taking a dependencies struct (component kinds, encoder/decoder execution contracts, decode policy, pinned tokenizer sha) and owning speech/encoder, speech/decoder, speech/tokenizer component facades (sm_any + kind enum), mirroring text/generator - promote execution contracts and ASR decode policy to component level; whisper variants alias them and keep binders at the variant boundary - add speech/tokenizer/whisper detokenize machine (route effect -> component actor dispatch, truncate-to-capacity contract preserved) - rewrite transcriber lifecycle tests against the dependencies API - ride-along fixes surfaced by gate scoping: stale sortformer test namespace, docsgen bracket-depth overflow on deep transition tables, clang-format drift (lint baseline strictly shrank, 11 entries removed) --- .../mermaid/speech_tokenizer_whisper.mmd | 26 + .../mermaid/speech_transcriber.mmd | 48 + .../mermaid/text_generator_initializer.mmd | 5 +- .../architecture/speech_tokenizer_whisper.md | 62 ++ .planning/architecture/speech_transcriber.md | 106 +++ .../text_generator_initializer.md | 10 +- CMakeLists.txt | 3 +- README.md | 2 + a.out | 0 scripts/check_domain_boundaries.sh | 27 +- snapshots/lint/clang_format.txt | 11 - snapshots/quality_gates/timing.txt | 16 +- .../diarization/sortformer/output/any.hpp | 2 +- .../diarization/sortformer/pipeline/any.hpp | 2 +- src/emel/kernel/aarch64/sm.hpp | 40 +- src/emel/machines.hpp | 4 +- src/emel/model/generation/any.cpp | 28 +- src/emel/model/generation/any.hpp | 72 +- src/emel/model/sortformer/any.hpp | 11 +- src/emel/speech/codec/mimi/detail.cpp | 10 +- src/emel/speech/codec/mimi/guards.hpp | 3 +- src/emel/speech/decoder/any.hpp | 48 + src/emel/speech/decoder/events.hpp | 103 +++ src/emel/speech/decoder/whisper/detail.hpp | 65 +- src/emel/speech/decoder/whisper/events.hpp | 91 +- src/emel/speech/encoder/any.hpp | 48 + src/emel/speech/encoder/events.hpp | 79 ++ src/emel/speech/encoder/whisper/detail.hpp | 11 +- src/emel/speech/encoder/whisper/events.hpp | 58 +- src/emel/speech/recognizer/context.hpp | 7 - .../recognizer_routes/whisper/actions.hpp | 88 -- .../speech/recognizer_routes/whisper/any.hpp | 25 - .../recognizer_routes/whisper/detail.cpp | 41 - .../recognizer_routes/whisper/guards.hpp | 71 -- src/emel/speech/tokenizer/any.hpp | 48 + src/emel/speech/tokenizer/events.hpp | 93 ++ src/emel/speech/tokenizer/whisper/actions.hpp | 70 ++ src/emel/speech/tokenizer/whisper/detail.hpp | 77 +- src/emel/speech/tokenizer/whisper/errors.hpp | 13 + src/emel/speech/tokenizer/whisper/events.hpp | 29 + src/emel/speech/tokenizer/whisper/guards.hpp | 61 ++ src/emel/speech/tokenizer/whisper/sm.hpp | 131 +++ .../{recognizer => transcriber}/actions.hpp | 105 ++- src/emel/speech/transcriber/context.hpp | 43 + .../{recognizer => transcriber}/detail.hpp | 6 +- .../{recognizer => transcriber}/errors.hpp | 4 +- .../{recognizer => transcriber}/events.hpp | 15 +- .../{recognizer => transcriber}/guards.hpp | 112 ++- .../speech/{recognizer => transcriber}/sm.hpp | 141 +-- .../sortformer/pipeline/lifecycle_tests.cpp | 4 +- tests/model/moshi/binding_tests.cpp | 175 ++-- tests/speech/codec/mimi_lifecycle_tests.cpp | 107 ++- .../decoder/whisper/lifecycle_tests.cpp | 308 +++---- .../encoder/whisper/lifecycle_tests.cpp | 58 +- tests/speech/recognizer/lifecycle_tests.cpp | 833 ------------------ tests/speech/transcriber/lifecycle_tests.cpp | 652 ++++++++++++++ tests/text/generator/detail_tests.cpp | 40 +- tools/bench/whisper_benchmark.py | 4 +- tools/bench/whisper_benchmark_tests.cpp | 62 +- tools/bench/whisper_compare.py | 4 +- tools/bench/whisper_emel_parity_runner.cpp | 84 +- tools/docsgen/CMakeLists.txt | 3 + tools/paritychecker/parity_engines.cpp | 12 +- 63 files changed, 2551 insertions(+), 1966 deletions(-) create mode 100644 .planning/architecture/mermaid/speech_tokenizer_whisper.mmd create mode 100644 .planning/architecture/mermaid/speech_transcriber.mmd create mode 100644 .planning/architecture/speech_tokenizer_whisper.md create mode 100644 .planning/architecture/speech_transcriber.md create mode 100755 a.out create mode 100644 src/emel/speech/decoder/any.hpp create mode 100644 src/emel/speech/decoder/events.hpp create mode 100644 src/emel/speech/encoder/any.hpp create mode 100644 src/emel/speech/encoder/events.hpp delete mode 100644 src/emel/speech/recognizer/context.hpp delete mode 100644 src/emel/speech/recognizer_routes/whisper/actions.hpp delete mode 100644 src/emel/speech/recognizer_routes/whisper/any.hpp delete mode 100644 src/emel/speech/recognizer_routes/whisper/detail.cpp delete mode 100644 src/emel/speech/recognizer_routes/whisper/guards.hpp create mode 100644 src/emel/speech/tokenizer/any.hpp create mode 100644 src/emel/speech/tokenizer/events.hpp create mode 100644 src/emel/speech/tokenizer/whisper/actions.hpp create mode 100644 src/emel/speech/tokenizer/whisper/errors.hpp create mode 100644 src/emel/speech/tokenizer/whisper/events.hpp create mode 100644 src/emel/speech/tokenizer/whisper/guards.hpp create mode 100644 src/emel/speech/tokenizer/whisper/sm.hpp rename src/emel/speech/{recognizer => transcriber}/actions.hpp (71%) create mode 100644 src/emel/speech/transcriber/context.hpp rename src/emel/speech/{recognizer => transcriber}/detail.hpp (50%) rename src/emel/speech/{recognizer => transcriber}/errors.hpp (76%) rename src/emel/speech/{recognizer => transcriber}/events.hpp (95%) rename src/emel/speech/{recognizer => transcriber}/guards.hpp (63%) rename src/emel/speech/{recognizer => transcriber}/sm.hpp (69%) delete mode 100644 tests/speech/recognizer/lifecycle_tests.cpp create mode 100644 tests/speech/transcriber/lifecycle_tests.cpp diff --git a/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd new file mode 100644 index 00000000..b7ea33cc --- /dev/null +++ b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd @@ -0,0 +1,26 @@ +stateDiagram-v2 + direction TB + [*] --> state_ready + state_ready --> state_json_decision : detokenize_run [always] / effect_begin_detokenize_ + state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_tokenizer_json_valid_] / effect_detokenize_ + state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_tokenizer_json_invalid_] / effect_mark_tokenizer_json_invalid_ + state_detokenizing --> state_success_error_out_decision : completion_detokenize_run_ [always] / none + state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ + state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none + state_error_error_out_decision --> state_error_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ + state_error_error_out_decision --> state_error_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none + state_success_callback_decision --> state_done : completion_detokenize_run_ [guard_has_done_callback_] / effect_emit_done_ + state_success_callback_decision --> state_done : completion_detokenize_run_ [guard_no_done_callback_] / none + state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_has_error_callback_] / effect_emit_error_ + state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_no_error_callback_] / none + state_done --> state_ready : completion_detokenize_run_ [always] / none + state_errored --> state_ready : completion_detokenize_run_ [always] / none + state_ready --> state_ready : _ [always] / effect_on_unexpected_ + state_json_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_detokenizing --> state_ready : _ [always] / effect_on_unexpected_ + state_success_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_success_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_error_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_error_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_done --> state_ready : _ [always] / effect_on_unexpected_ + state_errored --> state_ready : _ [always] / effect_on_unexpected_ diff --git a/.planning/architecture/mermaid/speech_transcriber.mmd b/.planning/architecture/mermaid/speech_transcriber.mmd new file mode 100644 index 00000000..6627cf77 --- /dev/null +++ b/.planning/architecture/mermaid/speech_transcriber.mmd @@ -0,0 +1,48 @@ +stateDiagram-v2 + direction TB + [*] --> state_uninitialized + state_uninitialized --> state_initializing : initialize_run [guard_valid_initialize_] / effect_begin_initialize_ + state_uninitialized --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ + state_ready --> state_initializing : initialize_run [guard_valid_initialize_] / effect_begin_initialize_ + state_ready --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ + state_errored --> state_initialize_error_out_decision : initialize_run [always] / effect_reject_initialize_ + state_initializing --> state_tokenizer_decision : completion_initialize_run_ [always] / none + state_tokenizer_decision --> state_model_support_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / none + state_tokenizer_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_tokenizer_unsupported_] / effect_mark_tokenizer_invalid_ + state_model_support_decision --> state_initialize_success : completion_initialize_run_ [guard_initialize_model_supported_] / none + state_model_support_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_unsupported_model_] / effect_mark_unsupported_model_ + state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_success_ + state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_no_initialize_error_out_] / none + state_initialize_error_out_decision --> state_initialize_error_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_error_ + state_initialize_error_out_decision --> state_initialize_error_callback_decision : completion_initialize_run_ [guard_no_initialize_error_out_] / none + state_initialize_done_callback_decision --> state_ready : completion_initialize_run_ [guard_has_initialize_done_callback_] / effect_emit_initialize_done_ + state_initialize_done_callback_decision --> state_ready : completion_initialize_run_ [guard_no_initialize_done_callback_] / none + state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_has_initialize_error_callback_] / effect_emit_initialize_error_ + state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_no_initialize_error_callback_] / none + state_ready --> state_recognize_support_decision : recognize_run [guard_valid_recognize_] / effect_begin_recognize_ + state_ready --> state_recognize_error_out_decision : recognize_run [guard_invalid_recognize_] / effect_reject_recognize_ + state_uninitialized --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_errored --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_recognize_support_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_transcriber_unsupported_] / effect_mark_uninitialized_ + state_recognize_support_decision --> state_encoding : completion_recognize_run_ [guard_transcriber_ready_] / effect_encode_ + state_encoding --> state_encoder_decision : completion_recognize_run_ [always] / none + state_encoder_decision --> state_decoding : completion_recognize_run_ [guard_encoder_success_] / effect_decode_ + state_encoder_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_encoder_failure_] / effect_mark_backend_error_ + state_decoding --> state_decoder_decision : completion_recognize_run_ [always] / none + state_decoder_decision --> state_detokenizing : completion_recognize_run_ [guard_decoder_success_] / effect_detokenize_ + state_decoder_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_decoder_failure_] / effect_mark_backend_error_ + state_detokenizing --> state_detokenize_decision : completion_recognize_run_ [always] / none + state_detokenize_decision --> state_recognize_success : completion_recognize_run_ [guard_detokenize_success_] / effect_publish_recognition_outputs_ + state_detokenize_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_detokenize_failure_] / effect_mark_backend_error_ + state_recognize_success --> state_recognize_done_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_success_ + state_recognize_success --> state_recognize_done_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_error_out_decision --> state_recognize_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_error_out_decision --> state_recognize_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_done_callback_decision --> state_done : completion_recognize_run_ [guard_has_recognize_done_callback_] / effect_emit_recognize_done_ + state_recognize_done_callback_decision --> state_done : completion_recognize_run_ [guard_no_recognize_done_callback_] / none + state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_no_recognize_error_callback_] / none + state_done --> state_ready : completion_recognize_run_ [always] / none + state_uninitialized --> state_uninitialized : _ [always] / effect_on_unexpected_ + state_ready --> state_ready : _ [always] / effect_on_unexpected_ + state_errored --> state_errored : _ [always] / effect_on_unexpected_ diff --git a/.planning/architecture/mermaid/text_generator_initializer.mmd b/.planning/architecture/mermaid/text_generator_initializer.mmd index e223b665..640e7aff 100644 --- a/.planning/architecture/mermaid/text_generator_initializer.mmd +++ b/.planning/architecture/mermaid/text_generator_initializer.mmd @@ -2,9 +2,10 @@ stateDiagram-v2 direction TB [*] --> idle idle --> preparing_backend : run [always] / begin_initialize_ - preparing_backend --> binding_conditioner : completion_run_ [backend_already_ready_] / none + preparing_backend --> binding_conditioner : completion_run_ [guard_backend_reuse_allowed_] / none preparing_backend_decision --> binding_conditioner : completion_run_ [backend_already_ready_] / none - preparing_backend --> preparing_backend_decision : completion_run_ [backend_prepare_needed_] / request_backend_prepare_ + preparing_backend --> idle : completion_run_ [guard_generation_contract_invalid_] / mark_invalid_request_ + preparing_backend --> preparing_backend_decision : completion_run_ [guard_backend_prepare_allowed_] / request_backend_prepare_ preparing_backend_decision --> binding_conditioner : completion_run_ [backend_prepare_ok_] / accept_prepared_backend_ preparing_backend_decision --> idle : completion_run_ [backend_prepare_invalid_request_] / mark_invalid_request_ preparing_backend_decision --> idle : completion_run_ [backend_prepare_backend_error_] / mark_backend_error_ diff --git a/.planning/architecture/speech_tokenizer_whisper.md b/.planning/architecture/speech_tokenizer_whisper.md new file mode 100644 index 00000000..23c981a8 --- /dev/null +++ b/.planning/architecture/speech_tokenizer_whisper.md @@ -0,0 +1,62 @@ +# speech_tokenizer_whisper + +Source: [`emel/speech/tokenizer/whisper/sm.hpp`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) + +## Mermaid + +```mermaid +stateDiagram-v2 + direction TB + [*] --> state_ready + state_ready --> state_json_decision : detokenize_run [always] / effect_begin_detokenize_ + state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_tokenizer_json_valid_] / effect_detokenize_ + state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_tokenizer_json_invalid_] / effect_mark_tokenizer_json_invalid_ + state_detokenizing --> state_success_error_out_decision : completion_detokenize_run_ [always] / none + state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ + state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none + state_error_error_out_decision --> state_error_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ + state_error_error_out_decision --> state_error_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none + state_success_callback_decision --> state_done : completion_detokenize_run_ [guard_has_done_callback_] / effect_emit_done_ + state_success_callback_decision --> state_done : completion_detokenize_run_ [guard_no_done_callback_] / none + state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_has_error_callback_] / effect_emit_error_ + state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_no_error_callback_] / none + state_done --> state_ready : completion_detokenize_run_ [always] / none + state_errored --> state_ready : completion_detokenize_run_ [always] / none + state_ready --> state_ready : _ [always] / effect_on_unexpected_ + state_json_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_detokenizing --> state_ready : _ [always] / effect_on_unexpected_ + state_success_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_success_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_error_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_error_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_done --> state_ready : _ [always] / effect_on_unexpected_ + state_errored --> state_ready : _ [always] / effect_on_unexpected_ +``` + +## Transitions + +| Source | Event | Guard | Action | Target | +| --- | --- | --- | --- | --- | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`detokenize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_begin_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_tokenizer_json_valid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_tokenizer_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_mark_tokenizer_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_has_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_store_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_has_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_store_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_has_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_emit_done>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_has_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_emit_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | diff --git a/.planning/architecture/speech_transcriber.md b/.planning/architecture/speech_transcriber.md new file mode 100644 index 00000000..55f21af3 --- /dev/null +++ b/.planning/architecture/speech_transcriber.md @@ -0,0 +1,106 @@ +# speech_transcriber + +Source: [`emel/speech/transcriber/sm.hpp`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) + +## Mermaid + +```mermaid +stateDiagram-v2 + direction TB + [*] --> state_uninitialized + state_uninitialized --> state_initializing : initialize_run [guard_valid_initialize_] / effect_begin_initialize_ + state_uninitialized --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ + state_ready --> state_initializing : initialize_run [guard_valid_initialize_] / effect_begin_initialize_ + state_ready --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ + state_errored --> state_initialize_error_out_decision : initialize_run [always] / effect_reject_initialize_ + state_initializing --> state_tokenizer_decision : completion_initialize_run_ [always] / none + state_tokenizer_decision --> state_model_support_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / none + state_tokenizer_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_tokenizer_unsupported_] / effect_mark_tokenizer_invalid_ + state_model_support_decision --> state_initialize_success : completion_initialize_run_ [guard_initialize_model_supported_] / none + state_model_support_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_unsupported_model_] / effect_mark_unsupported_model_ + state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_success_ + state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_no_initialize_error_out_] / none + state_initialize_error_out_decision --> state_initialize_error_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_error_ + state_initialize_error_out_decision --> state_initialize_error_callback_decision : completion_initialize_run_ [guard_no_initialize_error_out_] / none + state_initialize_done_callback_decision --> state_ready : completion_initialize_run_ [guard_has_initialize_done_callback_] / effect_emit_initialize_done_ + state_initialize_done_callback_decision --> state_ready : completion_initialize_run_ [guard_no_initialize_done_callback_] / none + state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_has_initialize_error_callback_] / effect_emit_initialize_error_ + state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_no_initialize_error_callback_] / none + state_ready --> state_recognize_support_decision : recognize_run [guard_valid_recognize_] / effect_begin_recognize_ + state_ready --> state_recognize_error_out_decision : recognize_run [guard_invalid_recognize_] / effect_reject_recognize_ + state_uninitialized --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_errored --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_recognize_support_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_transcriber_unsupported_] / effect_mark_uninitialized_ + state_recognize_support_decision --> state_encoding : completion_recognize_run_ [guard_transcriber_ready_] / effect_encode_ + state_encoding --> state_encoder_decision : completion_recognize_run_ [always] / none + state_encoder_decision --> state_decoding : completion_recognize_run_ [guard_encoder_success_] / effect_decode_ + state_encoder_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_encoder_failure_] / effect_mark_backend_error_ + state_decoding --> state_decoder_decision : completion_recognize_run_ [always] / none + state_decoder_decision --> state_detokenizing : completion_recognize_run_ [guard_decoder_success_] / effect_detokenize_ + state_decoder_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_decoder_failure_] / effect_mark_backend_error_ + state_detokenizing --> state_detokenize_decision : completion_recognize_run_ [always] / none + state_detokenize_decision --> state_recognize_success : completion_recognize_run_ [guard_detokenize_success_] / effect_publish_recognition_outputs_ + state_detokenize_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_detokenize_failure_] / effect_mark_backend_error_ + state_recognize_success --> state_recognize_done_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_success_ + state_recognize_success --> state_recognize_done_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_error_out_decision --> state_recognize_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_error_out_decision --> state_recognize_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_done_callback_decision --> state_done : completion_recognize_run_ [guard_has_recognize_done_callback_] / effect_emit_recognize_done_ + state_recognize_done_callback_decision --> state_done : completion_recognize_run_ [guard_no_recognize_done_callback_] / none + state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_no_recognize_error_callback_] / none + state_done --> state_ready : completion_recognize_run_ [always] / none + state_uninitialized --> state_uninitialized : _ [always] / effect_on_unexpected_ + state_ready --> state_ready : _ [always] / effect_on_unexpected_ + state_errored --> state_errored : _ [always] / effect_on_unexpected_ +``` + +## Transitions + +| Source | Event | Guard | Action | Target | +| --- | --- | --- | --- | --- | +| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_valid_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_begin_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initializing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_invalid_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_valid_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_begin_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initializing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_invalid_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initializing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_tokenizer_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_tokenizer_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_tokenizer_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_model_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_unsupported_model>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_unsupported_model>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_initialize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_initialize_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_initialize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_initialize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_initialize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_initialize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_initialize_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_initialize_done>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_initialize_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_initialize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_initialize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_initialize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_initialize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_valid_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_begin_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_invalid_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_transcriber_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_transcriber_ready>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_encode>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_encoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_encoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_encoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_encoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_encoder_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_decode>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_decoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_encoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_encoder_failure>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_backend_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_decoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_decoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_decoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_decoder_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_decoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_decoder_failure>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_backend_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_detokenize_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_detokenize_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_detokenize_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_publish_recognition_outputs>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_detokenize_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_detokenize_failure>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_backend_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_recognize_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_recognize_done>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_done_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | diff --git a/.planning/architecture/text_generator_initializer.md b/.planning/architecture/text_generator_initializer.md index cb5460cc..139b877a 100644 --- a/.planning/architecture/text_generator_initializer.md +++ b/.planning/architecture/text_generator_initializer.md @@ -9,9 +9,10 @@ stateDiagram-v2 direction TB [*] --> idle idle --> preparing_backend : run [always] / begin_initialize_ - preparing_backend --> binding_conditioner : completion_run_ [backend_already_ready_] / none + preparing_backend --> binding_conditioner : completion_run_ [guard_backend_reuse_allowed_] / none preparing_backend_decision --> binding_conditioner : completion_run_ [backend_already_ready_] / none - preparing_backend --> preparing_backend_decision : completion_run_ [backend_prepare_needed_] / request_backend_prepare_ + preparing_backend --> idle : completion_run_ [guard_generation_contract_invalid_] / mark_invalid_request_ + preparing_backend --> preparing_backend_decision : completion_run_ [guard_backend_prepare_allowed_] / request_backend_prepare_ preparing_backend_decision --> binding_conditioner : completion_run_ [backend_prepare_ok_] / accept_prepared_backend_ preparing_backend_decision --> idle : completion_run_ [backend_prepare_invalid_request_] / mark_invalid_request_ preparing_backend_decision --> idle : completion_run_ [backend_prepare_backend_error_] / mark_backend_error_ @@ -64,9 +65,10 @@ stateDiagram-v2 | Source | Event | Guard | Action | Target | | --- | --- | --- | --- | --- | | [`idle`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`begin_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | -| [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_already_ready>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`binding_conditioner`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | +| [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`guard_backend_reuse_allowed>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`binding_conditioner`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_already_ready>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`binding_conditioner`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | -| [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_prepare_needed>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`request_backend_prepare>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | +| [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`guard_generation_contract_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`mark_invalid_request>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`idle`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | +| [`preparing_backend`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`guard_backend_prepare_allowed>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`request_backend_prepare>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_prepare_ok>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`accept_prepared_backend>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`binding_conditioner`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_prepare_invalid_request>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`mark_invalid_request>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`idle`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | | [`preparing_backend_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`backend_prepare_backend_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`mark_backend_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | [`idle`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/text/generator/initializer/sm.hpp) | diff --git a/CMakeLists.txt b/CMakeLists.txt index 88ea8e16..2a94cea4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,7 +137,6 @@ add_library(emel STATIC src/emel/model/moshi/detail.cpp src/emel/embeddings/generator/omniembed/sm.cpp src/emel/speech/codec/mimi/detail.cpp - src/emel/speech/recognizer_routes/whisper/detail.cpp src/emel/diarization/sortformer/detail.cpp src/emel/diarization/sortformer/encoder/feature_extractor/detail.cpp src/emel/diarization/sortformer/encoder/detail.cpp @@ -192,7 +191,7 @@ if(EMEL_ENABLE_TESTS) tests/speech/encoder/whisper/lifecycle_tests.cpp tests/speech/decoder/whisper/lifecycle_tests.cpp tests/speech/encoder/whisper/detail_tests.cpp - tests/speech/recognizer/lifecycle_tests.cpp + tests/speech/transcriber/lifecycle_tests.cpp tests/speech/tokenizer/whisper_tests.cpp tests/embeddings/text_embedding_lane_tests.cpp tests/embeddings/vision_embedding_lane_tests.cpp diff --git a/README.md b/README.md index a817c2fd..4a8c1fe8 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,8 @@ environments, while Zig remains the default for day-to-day builds. - [`.planning/architecture/speech_codec_mimi.md`](.planning/architecture/speech_codec_mimi.md) - [`.planning/architecture/speech_decoder_whisper.md`](.planning/architecture/speech_decoder_whisper.md) - [`.planning/architecture/speech_encoder_whisper.md`](.planning/architecture/speech_encoder_whisper.md) +- [`.planning/architecture/speech_tokenizer_whisper.md`](.planning/architecture/speech_tokenizer_whisper.md) +- [`.planning/architecture/speech_transcriber.md`](.planning/architecture/speech_transcriber.md) - [`.planning/architecture/tensor_view.md`](.planning/architecture/tensor_view.md) - [`.planning/architecture/text_conditioner.md`](.planning/architecture/text_conditioner.md) - [`.planning/architecture/text_detokenizer.md`](.planning/architecture/text_detokenizer.md) diff --git a/a.out b/a.out new file mode 100755 index 00000000..e69de29b diff --git a/scripts/check_domain_boundaries.sh b/scripts/check_domain_boundaries.sh index 0a8a3981..13d6a944 100755 --- a/scripts/check_domain_boundaries.sh +++ b/scripts/check_domain_boundaries.sh @@ -59,7 +59,7 @@ cd "$ROOT_DIR" concrete_io_api_pattern='(^|[^[:alnum:]_])(::)?(mmap|munmap|pread|read|fread|fopen|fclose|open|openat|CreateFile|CreateFileMapping|MapViewOfFile|ReadFile)[[:space:]]*\(|std::(ifstream|fstream|filebuf|fread|fopen|freopen)' check_no_matches "forbidden model-family runtime roots" \ - 'emel/whisper|namespace emel::whisper|kernel/whisper|kernel::whisper|model/whisper/(runtime|inference|encoder|decoder)|model::whisper::(runtime|inference|encoder|decoder)|speech/asr/whisper|speech::asr::whisper|speech/whisper|speech::whisper|recognizer/detail/whisper|recognizer::detail::whisper' \ + 'emel/whisper|namespace emel::whisper|kernel/whisper|kernel::whisper|model/whisper/(runtime|inference|encoder|decoder)|model::whisper::(runtime|inference|encoder|decoder)|speech/asr/whisper|speech::asr::whisper|speech/whisper|speech::whisper|transcriber/detail/whisper|transcriber::detail::whisper' \ src tests tools CMakeLists.txt check_no_matches "qwen3 model code depends on sibling model-family detail" \ @@ -188,9 +188,9 @@ check_no_matches "forbidden moshi model-family runtime roots" \ 'emel/moshi|namespace emel::moshi|kernel/moshi|kernel::moshi|kernel/mimi|kernel::mimi|model/moshi/(runtime|inference|encoder|decoder|codec)|model::moshi::(runtime|inference|encoder|decoder|codec)|speech/asr/moshi|speech::asr::moshi|speech/moshi/|speech::moshi' \ src tests tools CMakeLists.txt -check_no_matches "Moshi/Mimi leaked into generic speech recognizer" \ +check_no_matches "Moshi/Mimi leaked into generic speech transcriber" \ 'moshi|model/moshi|model::moshi|tokenizer::moshi|codec::mimi|speech/codec/mimi' \ - src/emel/speech/recognizer tests/speech/recognizer + src/emel/speech/transcriber tests/speech/transcriber check_absent_path "retired generic diarization request owner path" \ src/emel/diarization/request \ @@ -200,13 +200,22 @@ check_absent_path "retired generic diarization request owner path" \ .planning/architecture/diarization_request.md \ .planning/architecture/mermaid/diarization_request.mmd -check_no_matches "Whisper leaked into generic speech recognizer" \ +check_no_matches "Whisper leaked into generic speech transcriber" \ 'whisper|model/whisper|speech/tokenizer/whisper|speech/encoder/whisper|speech/decoder/whisper|model::whisper|tokenizer::whisper|encoder::whisper|decoder::whisper' \ - src/emel/speech/recognizer tests/speech/recognizer - -check_no_matches "Whisper encoder detail leaked into recognizer route" \ - 'emel/speech/(encoder|decoder|tokenizer)/whisper/detail\.hpp|(encoder|decoder|tokenizer)::whisper::detail|using[[:space:]]+namespace[[:space:]]+[^;]*whisper|::detail([[:space:]]*(;|$)|::)|(^|[^[:alnum:]_:])detail::' \ - src/emel/speech/recognizer_routes/whisper + src/emel/speech/transcriber + +# Tests may select component variants through the component-level kind enums +# (encoder_kind::whisper), but must not include variant headers or reach into +# variant namespaces. +check_no_matches "Whisper contracts leaked into speech transcriber tests" \ + 'emel/speech/(encoder|decoder|tokenizer)/whisper|emel/model/whisper|(model|tokenizer|encoder|decoder)::whisper::' \ + tests/speech/transcriber + +check_no_matches "Variant detail leaked into speech component facades" \ + 'emel/speech/(encoder|decoder|tokenizer)/whisper/detail\.hpp|(encoder|decoder|tokenizer)::whisper::detail' \ + src/emel/speech/encoder/any.hpp src/emel/speech/encoder/events.hpp \ + src/emel/speech/decoder/any.hpp src/emel/speech/decoder/events.hpp \ + src/emel/speech/tokenizer/any.hpp src/emel/speech/tokenizer/events.hpp check_no_matches "Whisper model binding leaked into speech encoder/decoder runtime" \ 'emel/model/whisper|model::whisper' \ diff --git a/snapshots/lint/clang_format.txt b/snapshots/lint/clang_format.txt index 1bbce937..af4eae5a 100644 --- a/snapshots/lint/clang_format.txt +++ b/snapshots/lint/clang_format.txt @@ -291,7 +291,6 @@ src/emel/kernel/aarch64/context.hpp src/emel/kernel/aarch64/errors.hpp src/emel/kernel/aarch64/events.hpp src/emel/kernel/aarch64/guards.hpp -src/emel/kernel/aarch64/sm.hpp src/emel/kernel/actions.hpp src/emel/kernel/any.hpp src/emel/kernel/context.hpp @@ -372,21 +371,14 @@ src/emel/model/tensor/window/guards.hpp src/emel/model/tensor/window/sm.hpp src/emel/sm.hpp src/emel/speech/codec/mimi/any.hpp -src/emel/speech/codec/mimi/detail.cpp -src/emel/speech/codec/mimi/guards.hpp src/emel/speech/decoder/whisper/actions.hpp src/emel/speech/decoder/whisper/any.hpp src/emel/speech/decoder/whisper/context.hpp -src/emel/speech/decoder/whisper/detail.hpp src/emel/speech/decoder/whisper/errors.hpp -src/emel/speech/decoder/whisper/events.hpp src/emel/speech/encoder/whisper/any.hpp src/emel/speech/encoder/whisper/context.hpp src/emel/speech/encoder/whisper/errors.hpp src/emel/speech/encoder/whisper/guards.hpp -src/emel/speech/recognizer/detail.hpp -src/emel/speech/recognizer/errors.hpp -src/emel/speech/tokenizer/whisper/detail.hpp src/emel/tensor/view/actions.hpp src/emel/tensor/view/context.hpp src/emel/tensor/view/detail.hpp @@ -618,7 +610,6 @@ tests/memory/kv/lifecycle_tests.cpp tests/memory/recurrent/lifecycle_tests.cpp tests/model/fixture_manifest_tests.cpp tests/model/loader/lifecycle_tests.cpp -tests/model/moshi/binding_tests.cpp tests/model/tensor/lifecycle_tests.cpp tests/model/tensor/window/lifecycle_tests.cpp tests/model/tensor/window/streaming_tests.cpp @@ -627,8 +618,6 @@ tests/sm/callback_tests.cpp tests/sm/sm_any_tests.cpp tests/sm/sm_external_completion_tests.cpp tests/sm/sm_policy_tests.cpp -tests/speech/codec/mimi_lifecycle_tests.cpp -tests/speech/decoder/whisper/lifecycle_tests.cpp tests/speech/encoder/whisper/detail_tests.cpp tests/speech/tokenizer/whisper_tests.cpp tests/tensor/view/lifecycle_tests.cpp diff --git a/snapshots/quality_gates/timing.txt b/snapshots/quality_gates/timing.txt index 79dee1ee..4f18f4cb 100644 --- a/snapshots/quality_gates/timing.txt +++ b/snapshots/quality_gates/timing.txt @@ -1,14 +1,12 @@ # quality_gates timing (seconds) domain_boundaries 0 legacy_sml_surface 0 -build_with_zig 79 -bench_snapshot_generation 13 -bench_snapshot_kernel_aarch64 4 -bench_snapshot_parallel_matmul 49 -test_with_coverage 445 -paritychecker 75 +build_with_zig 1 +bench_snapshot 4 +test_with_coverage 186 +paritychecker 8 fuzz_smoke 0 -determinism_check 65 -lint_snapshot 14 +determinism_check 0 +lint_snapshot 12 generate_docs 27 -total 771 +total 226 diff --git a/src/emel/diarization/sortformer/output/any.hpp b/src/emel/diarization/sortformer/output/any.hpp index fe0efbf8..cddf86f4 100644 --- a/src/emel/diarization/sortformer/output/any.hpp +++ b/src/emel/diarization/sortformer/output/any.hpp @@ -24,4 +24,4 @@ struct segment_record { float max_probability = 0.0f; }; -} // namespace emel::diarization::sortformer::output +} // namespace emel::diarization::sortformer::output diff --git a/src/emel/diarization/sortformer/pipeline/any.hpp b/src/emel/diarization/sortformer/pipeline/any.hpp index 10ae5cb5..728b4dad 100644 --- a/src/emel/diarization/sortformer/pipeline/any.hpp +++ b/src/emel/diarization/sortformer/pipeline/any.hpp @@ -31,4 +31,4 @@ inline constexpr int32_t k_required_probability_value_count = inline constexpr int32_t k_max_segment_count = emel::diarization::sortformer::output::k_max_segment_count; -} // namespace emel::diarization::sortformer::pipeline +} // namespace emel::diarization::sortformer::pipeline diff --git a/src/emel/kernel/aarch64/sm.hpp b/src/emel/kernel/aarch64/sm.hpp index c6875f9f..b50fead4 100644 --- a/src/emel/kernel/aarch64/sm.hpp +++ b/src/emel/kernel/aarch64/sm.hpp @@ -1290,7 +1290,7 @@ struct sm : public emel::sm { using base_type = emel::sm; using base_type::base_type; - bool process_event(const ::emel::kernel::event::dispatch & ev) { + bool process_event(const ::emel::kernel::event::dispatch &ev) { event::dispatch_ctx ctx{}; const event::dispatch_request dispatch{ev, ctx}; return process_dispatch_event(dispatch); @@ -1298,7 +1298,7 @@ struct sm : public emel::sm { template requires(::emel::kernel::is_op_event_v) - bool process_event(const event_type & ev) { + bool process_event(const event_type &ev) { event::dispatch_ctx ctx{}; using dispatch_event_type = event::dispatch_event_for_t; const dispatch_event_type dispatch{ev, ctx}; @@ -1365,7 +1365,8 @@ struct sm : public emel::sm { return this->context_.optimized_q8_0_packed_bl8_dispatch_count; } - uint64_t optimized_q8_0_packed_bl8_full_groups_dispatch_count() const noexcept { + uint64_t + optimized_q8_0_packed_bl8_full_groups_dispatch_count() const noexcept { return this->context_.optimized_q8_0_packed_bl8_full_groups_dispatch_count; } @@ -1437,24 +1438,32 @@ struct sm : public emel::sm { return this->context_.optimized_q6_vector_packed_q8_rhs_dispatch_count; } - uint64_t optimized_q6_vector_packed_q8_rhs_argmax_dispatch_count() const noexcept { - return this->context_.optimized_q6_vector_packed_q8_rhs_argmax_dispatch_count; + uint64_t + optimized_q6_vector_packed_q8_rhs_argmax_dispatch_count() const noexcept { + return this->context_ + .optimized_q6_vector_packed_q8_rhs_argmax_dispatch_count; } uint64_t optimized_q6_vector_prepared_q8_rhs_dispatch_count() const noexcept { return this->context_.optimized_q6_vector_prepared_q8_rhs_dispatch_count; } - uint64_t optimized_q6_vector_prepared_q8_rhs_i8mm_dispatch_count() const noexcept { - return this->context_.optimized_q6_vector_prepared_q8_rhs_i8mm_dispatch_count; + uint64_t + optimized_q6_vector_prepared_q8_rhs_i8mm_dispatch_count() const noexcept { + return this->context_ + .optimized_q6_vector_prepared_q8_rhs_i8mm_dispatch_count; } - uint64_t optimized_q6_vector_prepared_q8_rhs_argmax_i8mm_dispatch_count() const noexcept { - return this->context_.optimized_q6_vector_prepared_q8_rhs_argmax_i8mm_dispatch_count; + uint64_t optimized_q6_vector_prepared_q8_rhs_argmax_i8mm_dispatch_count() + const noexcept { + return this->context_ + .optimized_q6_vector_prepared_q8_rhs_argmax_i8mm_dispatch_count; } - uint64_t optimized_q6_vector_q8_argmax_prepared_i8mm_dispatch_count() const noexcept { - return this->context_.optimized_q6_vector_q8_argmax_prepared_i8mm_dispatch_count; + uint64_t + optimized_q6_vector_q8_argmax_prepared_i8mm_dispatch_count() const noexcept { + return this->context_ + .optimized_q6_vector_q8_argmax_prepared_i8mm_dispatch_count; } uint64_t shared_q6_dispatch_count() const noexcept { @@ -1465,12 +1474,13 @@ struct sm : public emel::sm { return this->context_.shared_flash_dispatch_count; } - private: +private: template - bool process_dispatch_event(const dispatch_event_type & ev) { + bool process_dispatch_event(const dispatch_event_type &ev) { const bool accepted = base_type::process_event(ev); - return accepted && ev.ctx.err == static_cast(emel::error::cast(error::none)); + return accepted && + ev.ctx.err == static_cast(emel::error::cast(error::none)); } }; -} // namespace emel::kernel::aarch64 +} // namespace emel::kernel::aarch64 diff --git a/src/emel/machines.hpp b/src/emel/machines.hpp index 8a55ba27..91faf11b 100644 --- a/src/emel/machines.hpp +++ b/src/emel/machines.hpp @@ -1,11 +1,11 @@ #pragma once #include "emel/batch/planner/sm.hpp" -#include "emel/diarization/sortformer/request/sm.hpp" #include "emel/diarization/sortformer/executor/sm.hpp" #include "emel/diarization/sortformer/pipeline/sm.hpp" -#include "emel/embeddings/generator/sm.hpp" +#include "emel/diarization/sortformer/request/sm.hpp" #include "emel/embeddings/generator/omniembed/sm.hpp" +#include "emel/embeddings/generator/sm.hpp" #include "emel/gguf/loader/sm.hpp" #include "emel/graph/processor/sm.hpp" #include "emel/io/mmap/sm.hpp" diff --git a/src/emel/model/generation/any.cpp b/src/emel/model/generation/any.cpp index deee9e93..099058cc 100644 --- a/src/emel/model/generation/any.cpp +++ b/src/emel/model/generation/any.cpp @@ -70,8 +70,7 @@ bool block_has_required_tensors(const block_view &block) noexcept { tensor_view_bound(block.shortconv_out_proj); } -bool layer_requires_qk_norm( - const generation_layer_execution &layer) noexcept { +bool layer_requires_qk_norm(const generation_layer_execution &layer) noexcept { return layer.residual_route == generation_residual_route::attention && layer.qk_norm_route != generation_attention_qk_norm_route::none; } @@ -215,7 +214,7 @@ stage_tensor(const execution_view &execution, const block_view *block, return nullptr; } -} // namespace +} // namespace emel::error::type build_contract(const emel::model::data &model_data, contract &contract_out) noexcept { @@ -292,10 +291,11 @@ bool reject_block_tensor(const emel::model::data &model_data, !has_tensor_named(model_data, name); } -emel::error::type bind_attention_block( - const emel::model::data &model_data, const int32_t block_index, - const bool require_qk_norm, const bool use_shared_key_value, - block_view &block_out) noexcept { +emel::error::type bind_attention_block(const emel::model::data &model_data, + const int32_t block_index, + const bool require_qk_norm, + const bool use_shared_key_value, + block_view &block_out) noexcept { block_out = {}; block_out.index = block_index; block_out.uses_attention = true; @@ -342,9 +342,9 @@ emel::error::type bind_attention_block( return emel::error::cast(emel::model::loader::error::none); } -emel::error::type bind_shortconv_block( - const emel::model::data &model_data, const int32_t block_index, - block_view &block_out) noexcept { +emel::error::type bind_shortconv_block(const emel::model::data &model_data, + const int32_t block_index, + block_view &block_out) noexcept { block_out = {}; block_out.index = block_index; block_out.uses_attention = false; @@ -556,9 +556,9 @@ emel::error::type complete_contract(contract &contract_out) noexcept { return emel::error::cast(emel::model::loader::error::invalid_request); } - const auto step_err = build_step_plans(contract_out.topology, - contract_out.prefill_plan, - contract_out.decode_plan); + const auto step_err = + build_step_plans(contract_out.topology, contract_out.prefill_plan, + contract_out.decode_plan); if (step_err != emel::error::cast(emel::model::loader::error::none)) { return step_err; } @@ -646,4 +646,4 @@ build_quantized_path_audit(const execution_view &execution) noexcept { return audit; } -} // namespace emel::model::generation +} // namespace emel::model::generation diff --git a/src/emel/model/generation/any.hpp b/src/emel/model/generation/any.hpp index cc29fc7f..d521cf68 100644 --- a/src/emel/model/generation/any.hpp +++ b/src/emel/model/generation/any.hpp @@ -13,7 +13,7 @@ namespace emel::model::generation { constexpr uint32_t k_quantized_stage_family_count = 14u; struct tensor_view { - const emel::model::data::tensor_record * tensor = nullptr; + const emel::model::data::tensor_record *tensor = nullptr; std::string_view name = {}; }; @@ -40,7 +40,7 @@ struct execution_view { static constexpr uint32_t k_max_blocks = static_cast(emel::model::data::k_max_metadata_arrays); - const emel::model::data * model = nullptr; + const emel::model::data *model = nullptr; tensor_view token_embedding = {}; tensor_view output_norm = {}; tensor_view output = {}; @@ -79,13 +79,13 @@ struct generation_execution_descriptor { static constexpr uint32_t k_max_layers = static_cast(emel::model::data::k_max_metadata_arrays); - const execution_view * execution = nullptr; + const execution_view *execution = nullptr; uint32_t layer_count = 0u; std::array layers = {}; }; struct topology { - const execution_view * execution = nullptr; + const execution_view *execution = nullptr; uint32_t node_count = 0u; uint32_t tensor_count = 0u; uint64_t bytes_per_tensor = 0u; @@ -98,7 +98,7 @@ enum class step_kind : uint8_t { }; struct step_plan { - const topology * graph = nullptr; + const topology *graph = nullptr; step_kind kind = step_kind::prefill; uint32_t node_count = 0u; uint32_t tensor_count = 0u; @@ -146,9 +146,9 @@ struct quantized_path_audit { struct contract { contract() noexcept = default; contract(const contract &) = delete; - contract & operator=(const contract &) = delete; + contract &operator=(const contract &) = delete; contract(contract &&) = delete; - contract & operator=(contract &&) = delete; + contract &operator=(contract &&) = delete; ~contract() = default; void reset() noexcept { @@ -168,49 +168,45 @@ struct contract { quantized_path_audit quantized_audit = {}; }; -emel::error::type build_contract(const emel::model::data & model_data, - contract & contract_out) noexcept; -bool has_tensor_named(const emel::model::data & model_data, +emel::error::type build_contract(const emel::model::data &model_data, + contract &contract_out) noexcept; +bool has_tensor_named(const emel::model::data &model_data, std::string_view name) noexcept; -bool bind_tensor_view(const emel::model::data & model_data, - std::string_view name, - tensor_view & view_out) noexcept; -bool bind_output_view(const emel::model::data & model_data, - const tensor_view & token_embedding, - bool allow_tied_output, - tensor_view & output_out) noexcept; -bool bind_block_tensor_view(const emel::model::data & model_data, - int32_t block_index, - std::string_view suffix, - tensor_view & view_out) noexcept; -bool require_block_tensor(const emel::model::data & model_data, +bool bind_tensor_view(const emel::model::data &model_data, + std::string_view name, tensor_view &view_out) noexcept; +bool bind_output_view(const emel::model::data &model_data, + const tensor_view &token_embedding, + bool allow_tied_output, tensor_view &output_out) noexcept; +bool bind_block_tensor_view(const emel::model::data &model_data, + int32_t block_index, std::string_view suffix, + tensor_view &view_out) noexcept; +bool require_block_tensor(const emel::model::data &model_data, int32_t block_index, std::string_view suffix) noexcept; -bool reject_block_tensor(const emel::model::data & model_data, - int32_t block_index, - std::string_view suffix) noexcept; -emel::error::type bind_attention_block(const emel::model::data & model_data, +bool reject_block_tensor(const emel::model::data &model_data, + int32_t block_index, std::string_view suffix) noexcept; +emel::error::type bind_attention_block(const emel::model::data &model_data, int32_t block_index, bool require_qk_norm, bool use_shared_key_value, - block_view & block_out) noexcept; -emel::error::type bind_shortconv_block(const emel::model::data & model_data, + block_view &block_out) noexcept; +emel::error::type bind_shortconv_block(const emel::model::data &model_data, int32_t block_index, - block_view & block_out) noexcept; -emel::error::type lookup_block_view(const execution_view & execution, + block_view &block_out) noexcept; +emel::error::type lookup_block_view(const execution_view &execution, int32_t block_index, - block_view & block_out) noexcept; -emel::error::type build_step_plans(const topology & topology_in, - step_plan & prefill_out, - step_plan & decode_out) noexcept; -emel::error::type validate_contract(const contract & contract_in) noexcept; -emel::error::type complete_contract(contract & contract_out) noexcept; + block_view &block_out) noexcept; +emel::error::type build_step_plans(const topology &topology_in, + step_plan &prefill_out, + step_plan &decode_out) noexcept; +emel::error::type validate_contract(const contract &contract_in) noexcept; +emel::error::type complete_contract(contract &contract_out) noexcept; quantized_path_audit -build_quantized_path_audit(const execution_view & execution) noexcept; +build_quantized_path_audit(const execution_view &execution) noexcept; std::string_view quantized_stage_family_name(quantized_stage_family family) noexcept; std::string_view quantized_contract_kind_name(quantized_contract_kind kind) noexcept; std::string_view tensor_type_name(int32_t tensor_type) noexcept; -} // namespace emel::model::generation +} // namespace emel::model::generation diff --git a/src/emel/model/sortformer/any.hpp b/src/emel/model/sortformer/any.hpp index 7913356f..d79cb231 100644 --- a/src/emel/model/sortformer/any.hpp +++ b/src/emel/model/sortformer/any.hpp @@ -9,7 +9,7 @@ namespace emel::model::sortformer { struct tensor_view { - const emel::model::data::tensor_record * tensor = nullptr; + const emel::model::data::tensor_record *tensor = nullptr; std::string_view name = {}; }; @@ -20,7 +20,7 @@ struct family_view { }; struct execution_contract { - const emel::model::data * model = nullptr; + const emel::model::data *model = nullptr; int32_t sample_rate = 0; int32_t speaker_count = 0; int32_t frame_shift_ms = 0; @@ -35,7 +35,8 @@ struct execution_contract { family_view transformer_encoder = {}; }; -emel::error::type build_execution_contract(const emel::model::data & model_data, - execution_contract & contract_out) noexcept; +emel::error::type +build_execution_contract(const emel::model::data &model_data, + execution_contract &contract_out) noexcept; -} // namespace emel::model::sortformer +} // namespace emel::model::sortformer diff --git a/src/emel/speech/codec/mimi/detail.cpp b/src/emel/speech/codec/mimi/detail.cpp index d698c0ef..17f047bb 100644 --- a/src/emel/speech/codec/mimi/detail.cpp +++ b/src/emel/speech/codec/mimi/detail.cpp @@ -60,8 +60,7 @@ tensor_elements(const emel::model::data::tensor_record &tensor) noexcept { uint64_t count = 1; for (int32_t dim = 0; dim < tensor.n_dims; ++dim) { const int64_t extent = tensor.dims[static_cast(dim)]; - if (extent <= 0 || - static_cast(extent) > UINT64_MAX / count) { + if (extent <= 0 || static_cast(extent) > UINT64_MAX / count) { return 0u; } count *= static_cast(extent); @@ -2026,10 +2025,9 @@ bool bind_transformer(arena_cursor_t &cursor, uint64_t &state_cursor, template bool bind_rvq_split(arena_cursor_t &cursor, - const emel::model::data &model_data, - const char *split, const int32_t level_count, - const bool with_f16, const bool with_q8, - rvq_split_weights &split_out) noexcept { + const emel::model::data &model_data, const char *split, + const int32_t level_count, const bool with_f16, + const bool with_q8, rvq_split_weights &split_out) noexcept { // The per-level codebook/search-table arrays are k_max_quantizer_levels // wide; a split larger than that is out of contract regardless of which // tensors the model carries. diff --git a/src/emel/speech/codec/mimi/guards.hpp b/src/emel/speech/codec/mimi/guards.hpp index fd5d723c..dc7caf9d 100644 --- a/src/emel/speech/codec/mimi/guards.hpp +++ b/src/emel/speech/codec/mimi/guards.hpp @@ -38,7 +38,8 @@ struct guard_arena_capacity_valid { request.frame.data() != nullptr && request.prepared.size() >= detail::required_prepared_floats(model) && request.state_arena.size() >= detail::required_state_floats(model) && - request.workspace.size() >= detail::required_workspace_floats(model) && + request.workspace.size() >= + detail::required_workspace_floats(model) && request.frame.size() >= detail::required_frame_floats(model); } }; diff --git a/src/emel/speech/decoder/any.hpp b/src/emel/speech/decoder/any.hpp new file mode 100644 index 00000000..514b76b7 --- /dev/null +++ b/src/emel/speech/decoder/any.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "emel/sm.hpp" +#include "emel/speech/decoder/events.hpp" +#include "emel/speech/decoder/whisper/sm.hpp" + +namespace emel::speech::decoder { + +// Component variant selector. `unsupported` is a sentinel used by owners (e.g. +// speech/transcriber dependencies) to model "no decoder injected"; owners must +// guard dispatch on a supported kind. sm_any clamps out-of-range kinds to the +// first variant, so constructing a facade with `unsupported` is safe as long as +// no event is dispatched to it. +enum class decoder_kind : uint8_t { + whisper = 0, + unsupported = 255, +}; + +class any { +public: + any() = default; + explicit any(const decoder_kind kind) : core_(kind) {} + + any(const any &) = delete; + any &operator=(const any &) = delete; + any(any &&) = delete; + any &operator=(any &&) = delete; + + ~any() = default; + + void set_kind(const decoder_kind kind) { core_.set_kind(kind); } + + decoder_kind kind() const noexcept { return core_.kind(); } + + bool process_event(const event::decode &ev) { + return core_.process_event(ev); + } + +private: + using sm_list = stateforward::sml::aux::type_list; + using event_list = stateforward::sml::aux::type_list; + + emel::sm_any core_{}; +}; + +} // namespace emel::speech::decoder diff --git a/src/emel/speech/decoder/events.hpp b/src/emel/speech/decoder/events.hpp new file mode 100644 index 00000000..bef16b69 --- /dev/null +++ b/src/emel/speech/decoder/events.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include + +#include "emel/callback.hpp" +#include "emel/error/error.hpp" +#include "emel/model/data.hpp" +#include "emel/speech/tokenizer/events.hpp" + +namespace emel::speech::decoder { + +// Component execution contract: variant-neutral decoder dimensions bound from a +// loaded model by a variant binder (e.g. +// decoder::whisper::bind_execution_contract) at the variant boundary. The +// component machines validate the bound values before running; the contract +// itself carries no behavior. +struct execution_contract { + const emel::model::data *model = nullptr; + int32_t vocab_size = 0; + int32_t embedding_length = 0; + int32_t decoder_block_count = 0; +}; + +} // namespace emel::speech::decoder + +namespace emel::speech::decoder::events { + +struct decode_done; +struct decode_error; + +} // namespace emel::speech::decoder::events + +namespace emel::speech::decoder::event { + +struct decode { + decode(const execution_contract &contract_ref, + std::span encoder_state_ref, + const int32_t encoder_frame_count_ref, + const emel::speech::tokenizer::asr_decode_policy &policy_ref, + std::span workspace_ref, std::span logits_ref, + int32_t &token_out_ref, float &confidence_out_ref, + uint64_t &digest_out_ref) noexcept + : contract(contract_ref), encoder_state(encoder_state_ref), + encoder_frame_count(encoder_frame_count_ref), policy(policy_ref), + generated_token_storage{}, generated_tokens(generated_token_storage), + generated_token_count_storage(0), + generated_token_count_out(generated_token_count_storage), + workspace(workspace_ref), logits(logits_ref), token_out(token_out_ref), + confidence_out(confidence_out_ref), digest_out(digest_out_ref) {} + + decode(const execution_contract &contract_ref, + std::span encoder_state_ref, + const int32_t encoder_frame_count_ref, + const emel::speech::tokenizer::asr_decode_policy &policy_ref, + std::span generated_tokens_ref, + int32_t &generated_token_count_out_ref, std::span workspace_ref, + std::span logits_ref, int32_t &token_out_ref, + float &confidence_out_ref, uint64_t &digest_out_ref) noexcept + : contract(contract_ref), encoder_state(encoder_state_ref), + encoder_frame_count(encoder_frame_count_ref), policy(policy_ref), + generated_token_storage{}, generated_tokens(generated_tokens_ref), + generated_token_count_storage(0), + generated_token_count_out(generated_token_count_out_ref), + workspace(workspace_ref), logits(logits_ref), token_out(token_out_ref), + confidence_out(confidence_out_ref), digest_out(digest_out_ref) {} + + const execution_contract &contract; + std::span encoder_state = {}; + int32_t encoder_frame_count = 0; + const emel::speech::tokenizer::asr_decode_policy &policy; + std::array generated_token_storage = {}; + std::span generated_tokens = {}; + int32_t generated_token_count_storage = 0; + int32_t &generated_token_count_out; + std::span workspace = {}; + std::span logits = {}; + int32_t &token_out; + float &confidence_out; + uint64_t &digest_out; + emel::error::type *error_out = nullptr; + emel::callback on_done = {}; + emel::callback on_error = {}; +}; + +} // namespace emel::speech::decoder::event + +namespace emel::speech::decoder::events { + +struct decode_done { + const event::decode *request = nullptr; + int32_t token = 0; + float confidence = 0.0f; + uint64_t digest = 0; +}; + +struct decode_error { + const event::decode *request = nullptr; + emel::error::type err = {}; +}; + +} // namespace emel::speech::decoder::events diff --git a/src/emel/speech/decoder/whisper/detail.hpp b/src/emel/speech/decoder/whisper/detail.hpp index 215898bc..eb691620 100644 --- a/src/emel/speech/decoder/whisper/detail.hpp +++ b/src/emel/speech/decoder/whisper/detail.hpp @@ -14,6 +14,7 @@ #include "emel/kernel/sm.hpp" #include "emel/model/data.hpp" +#include "emel/speech/decoder/events.hpp" #include "emel/speech/decoder/whisper/errors.hpp" namespace emel::speech::decoder::whisper::detail { @@ -45,12 +46,7 @@ inline constexpr int32_t k_token_timestamp_begin = 50364; inline constexpr int32_t k_token_space = 220; inline constexpr float k_layer_norm_epsilon = 1.0e-5f; -struct execution_contract { - const emel::model::data *model = nullptr; - int32_t vocab_size = 0; - int32_t embedding_length = 0; - int32_t decoder_block_count = 0; -}; +using execution_contract = emel::speech::decoder::execution_contract; inline execution_contract bind_execution_contract(const emel::model::data &model) noexcept { @@ -328,7 +324,7 @@ linear_q8_0_quantized_input(::emel::kernel::sm &kernel, }, }, }; - (void) kernel.process_event(request); + (void)kernel.process_event(request); if constexpr (HasBias) { for (uint64_t row = 0; row < Out; ++row) { output[row] += read_aux_vector(*bias, row); @@ -381,7 +377,7 @@ inline void linear(::emel::kernel::sm &kernel, linear_q8_0_quantized_input(kernel, weight, &bias, input, output); } else { - (void) kernel; + (void)kernel; for (uint64_t row = 0; row < Out; ++row) { output[row] = dot_linear_row(weight, row, input, In) + read_aux_vector(bias, row); @@ -397,7 +393,7 @@ inline void linear_no_bias(::emel::kernel::sm &kernel, linear_q8_0_quantized_input(kernel, weight, nullptr, input, output); } else { - (void) kernel; + (void)kernel; for (uint64_t row = 0; row < Out; ++row) { output[row] = dot_linear_row(weight, row, input, In); } @@ -475,14 +471,12 @@ inline void compute_decoder_cross_cache(::emel::kernel::sm &kernel, } template -inline void -run_decoder_layer_sequence(::emel::kernel::sm &kernel, - const emel::model::data &model, const uint64_t layer, - const uint64_t encoder_frames, - const uint64_t token_count, const float *cross_k, - const float *cross_v, float *hidden, float *next, - float *q, float *k, float *v, float *attn, - float *norm, float *ff, float *scores) noexcept { +inline void run_decoder_layer_sequence( + ::emel::kernel::sm &kernel, const emel::model::data &model, + const uint64_t layer, const uint64_t encoder_frames, + const uint64_t token_count, const float *cross_k, const float *cross_v, + float *hidden, float *next, float *q, float *k, float *v, float *attn, + float *norm, float *ff, float *scores) noexcept { char name[96] = {}; const auto layer_tensor = [&](const char *suffix) noexcept { const uint64_t name_size = @@ -627,11 +621,11 @@ inline uint64_t digest_f32(const float *values, const uint64_t count) noexcept { template inline void compute_decoder_logits_for_tokens( - ::emel::kernel::sm &kernel, - const emel::model::data &model, const uint64_t encoder_frames, - const float *cross_k_cache, const float *cross_v_cache, - const int32_t *tokens, const uint64_t token_count, float *workspace, - float *logits, float &confidence_out, uint64_t &digest_out) noexcept { + ::emel::kernel::sm &kernel, const emel::model::data &model, + const uint64_t encoder_frames, const float *cross_k_cache, + const float *cross_v_cache, const int32_t *tokens, + const uint64_t token_count, float *workspace, float *logits, + float &confidence_out, uint64_t &digest_out) noexcept { float *hidden = workspace; const uint64_t hidden_count = static_cast(k_embedding_length) * token_count; @@ -771,8 +765,8 @@ inline int32_t select_greedy_timestamp_aware_token( (last_was_timestamp && penultimate_was_timestamp && timestamp_token) || (last_was_timestamp && !penultimate_was_timestamp && token < policy.eot); - const bool blocked_by_initial_limit = initial_token && timestamp_token && - token > policy.timestamp_begin + 50; + const bool blocked_by_initial_limit = + initial_token && timestamp_token && token > policy.timestamp_begin + 50; const bool blocked_by_timestamp_mass = force_timestamp && !timestamp_token; const float score = initial_suppressed || control_suppressed || blocked_by_pair_rule || @@ -791,12 +785,11 @@ inline int32_t select_greedy_timestamp_aware_token( template inline uint64_t run_decoder_sequence( - ::emel::kernel::sm &kernel, - const emel::model::data &model, const float *encoder_state, - const uint64_t encoder_frames, const decode_policy_runtime &policy, - const int32_t *prompt_tokens, const uint64_t prompt_token_count, - float *workspace, float *logits, int32_t *generated_tokens, - const uint64_t generated_token_capacity, + ::emel::kernel::sm &kernel, const emel::model::data &model, + const float *encoder_state, const uint64_t encoder_frames, + const decode_policy_runtime &policy, const int32_t *prompt_tokens, + const uint64_t prompt_token_count, float *workspace, float *logits, + int32_t *generated_tokens, const uint64_t generated_token_capacity, uint64_t &generated_token_count_out, int32_t &token_out, float &confidence_out) noexcept { std::array(k_decoder_sequence_token_count)> @@ -814,9 +807,9 @@ inline uint64_t run_decoder_sequence( float *cross_k_cache = workspace; float *cross_v_cache = cross_k_cache + cross_cache_count; float *step_workspace = cross_v_cache + cross_cache_count; - compute_decoder_cross_cache( - kernel, model, encoder_state, encoder_frames, cross_k_cache, - cross_v_cache); + compute_decoder_cross_cache(kernel, model, encoder_state, + encoder_frames, cross_k_cache, + cross_v_cache); uint64_t token_count = prompt_token_count; uint64_t digest = 0u; generated_token_count_out = 0u; @@ -824,8 +817,8 @@ inline uint64_t run_decoder_sequence( float raw_confidence = 0.0f; compute_decoder_logits_for_tokens( kernel, model, encoder_frames, cross_k_cache, cross_v_cache, - tokens.data(), - token_count, step_workspace, logits, raw_confidence, digest); + tokens.data(), token_count, step_workspace, logits, raw_confidence, + digest); const int32_t next_token = select_greedy_timestamp_aware_token( policy, logits, generated_tokens, step, step == 0u, confidence_out); token_out = next_token; @@ -841,4 +834,4 @@ inline uint64_t run_decoder_sequence( return digest; } -} // namespace emel::speech::decoder::whisper::detail +} // namespace emel::speech::decoder::whisper::detail diff --git a/src/emel/speech/decoder/whisper/events.hpp b/src/emel/speech/decoder/whisper/events.hpp index a241497e..7e4b3b9f 100644 --- a/src/emel/speech/decoder/whisper/events.hpp +++ b/src/emel/speech/decoder/whisper/events.hpp @@ -1,84 +1,21 @@ #pragma once -#include -#include -#include - -#include "emel/callback.hpp" #include "emel/error/error.hpp" +#include "emel/speech/decoder/events.hpp" #include "emel/speech/decoder/whisper/errors.hpp" -#include "emel/speech/tokenizer/whisper/any.hpp" - -namespace emel::speech::decoder::whisper::detail { - -struct execution_contract; - -} // namespace emel::speech::decoder::whisper::detail namespace emel::speech::decoder::whisper::events { -struct decode_done; -struct decode_error; +// The whisper variant consumes the component-level event vocabulary directly; +// the component contract carries the variant-bound dimensions. +using decode_done = emel::speech::decoder::events::decode_done; +using decode_error = emel::speech::decoder::events::decode_error; } // namespace emel::speech::decoder::whisper::events namespace emel::speech::decoder::whisper::event { -struct decode { - decode(const detail::execution_contract &contract_ref, - std::span encoder_state_ref, - const int32_t encoder_frame_count_ref, - const emel::speech::tokenizer::whisper::asr_decode_policy - &policy_ref, - std::span workspace_ref, std::span logits_ref, - int32_t &token_out_ref, float &confidence_out_ref, - uint64_t &digest_out_ref) noexcept - : contract(contract_ref), encoder_state(encoder_state_ref), - encoder_frame_count(encoder_frame_count_ref), - policy(policy_ref), - generated_token_storage{}, generated_tokens(generated_token_storage), - generated_token_count_storage(0), - generated_token_count_out(generated_token_count_storage), - workspace(workspace_ref), logits(logits_ref), - token_out(token_out_ref), confidence_out(confidence_out_ref), - digest_out(digest_out_ref) {} - - decode(const detail::execution_contract &contract_ref, - std::span encoder_state_ref, - const int32_t encoder_frame_count_ref, - const emel::speech::tokenizer::whisper::asr_decode_policy - &policy_ref, - std::span generated_tokens_ref, - int32_t &generated_token_count_out_ref, std::span workspace_ref, - std::span logits_ref, int32_t &token_out_ref, - float &confidence_out_ref, uint64_t &digest_out_ref) noexcept - : contract(contract_ref), encoder_state(encoder_state_ref), - encoder_frame_count(encoder_frame_count_ref), - policy(policy_ref), generated_token_storage{}, - generated_tokens(generated_tokens_ref), - generated_token_count_storage(0), - generated_token_count_out(generated_token_count_out_ref), - workspace(workspace_ref), logits(logits_ref), - token_out(token_out_ref), confidence_out(confidence_out_ref), - digest_out(digest_out_ref) {} - - const detail::execution_contract &contract; - std::span encoder_state = {}; - int32_t encoder_frame_count = 0; - const emel::speech::tokenizer::whisper::asr_decode_policy &policy; - std::array generated_token_storage = {}; - std::span generated_tokens = {}; - int32_t generated_token_count_storage = 0; - int32_t &generated_token_count_out; - std::span workspace = {}; - std::span logits = {}; - int32_t &token_out; - float &confidence_out; - uint64_t &digest_out; - emel::error::type *error_out = nullptr; - emel::callback on_done = {}; - emel::callback on_error = {}; -}; +using decode = emel::speech::decoder::event::decode; struct decode_ctx { emel::error::type err = emel::error::cast(error::none); @@ -90,19 +27,3 @@ struct decode_run { }; } // namespace emel::speech::decoder::whisper::event - -namespace emel::speech::decoder::whisper::events { - -struct decode_done { - const event::decode *request = nullptr; - int32_t token = 0; - float confidence = 0.0f; - uint64_t digest = 0; -}; - -struct decode_error { - const event::decode *request = nullptr; - emel::error::type err = emel::error::cast(error::none); -}; - -} // namespace emel::speech::decoder::whisper::events diff --git a/src/emel/speech/encoder/any.hpp b/src/emel/speech/encoder/any.hpp new file mode 100644 index 00000000..20228e12 --- /dev/null +++ b/src/emel/speech/encoder/any.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "emel/sm.hpp" +#include "emel/speech/encoder/events.hpp" +#include "emel/speech/encoder/whisper/sm.hpp" + +namespace emel::speech::encoder { + +// Component variant selector. `unsupported` is a sentinel used by owners (e.g. +// speech/transcriber dependencies) to model "no encoder injected"; owners must +// guard dispatch on a supported kind. sm_any clamps out-of-range kinds to the +// first variant, so constructing a facade with `unsupported` is safe as long as +// no event is dispatched to it. +enum class encoder_kind : uint8_t { + whisper = 0, + unsupported = 255, +}; + +class any { +public: + any() = default; + explicit any(const encoder_kind kind) : core_(kind) {} + + any(const any &) = delete; + any &operator=(const any &) = delete; + any(any &&) = delete; + any &operator=(any &&) = delete; + + ~any() = default; + + void set_kind(const encoder_kind kind) { core_.set_kind(kind); } + + encoder_kind kind() const noexcept { return core_.kind(); } + + bool process_event(const event::encode &ev) { + return core_.process_event(ev); + } + +private: + using sm_list = stateforward::sml::aux::type_list; + using event_list = stateforward::sml::aux::type_list; + + emel::sm_any core_{}; +}; + +} // namespace emel::speech::encoder diff --git a/src/emel/speech/encoder/events.hpp b/src/emel/speech/encoder/events.hpp new file mode 100644 index 00000000..b20e63e7 --- /dev/null +++ b/src/emel/speech/encoder/events.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include "emel/callback.hpp" +#include "emel/error/error.hpp" +#include "emel/model/data.hpp" + +namespace emel::speech::encoder { + +// Component execution contract: variant-neutral encoder dimensions bound from a +// loaded model by a variant binder (e.g. +// encoder::whisper::bind_execution_contract) at the variant boundary. The +// component machines validate the bound values before running; the contract +// itself carries no behavior. +struct execution_contract { + const emel::model::data *model = nullptr; + int32_t sample_rate = 0; + int32_t mel_bin_count = 0; + int32_t embedding_length = 0; + int32_t feed_forward_length = 0; + int32_t attention_head_count = 0; + int32_t encoder_block_count = 0; +}; + +} // namespace emel::speech::encoder + +namespace emel::speech::encoder::events { + +struct encode_done; +struct encode_error; + +} // namespace emel::speech::encoder::events + +namespace emel::speech::encoder::event { + +struct encode { + encode(const execution_contract &contract_ref, std::span pcm_ref, + const int32_t sample_rate_ref, const int32_t channel_count_ref, + std::span workspace_ref, std::span encoder_state_ref, + int32_t &frame_count_out_ref, int32_t &width_out_ref, + uint64_t &digest_out_ref) noexcept + : contract(contract_ref), pcm(pcm_ref), sample_rate(sample_rate_ref), + channel_count(channel_count_ref), workspace(workspace_ref), + encoder_state(encoder_state_ref), frame_count_out(frame_count_out_ref), + width_out(width_out_ref), digest_out(digest_out_ref) {} + + const execution_contract &contract; + std::span pcm = {}; + int32_t sample_rate = 0; + int32_t channel_count = 0; + std::span workspace = {}; + std::span encoder_state = {}; + int32_t &frame_count_out; + int32_t &width_out; + uint64_t &digest_out; + emel::error::type *error_out = nullptr; + emel::callback on_done = {}; + emel::callback on_error = {}; +}; + +} // namespace emel::speech::encoder::event + +namespace emel::speech::encoder::events { + +struct encode_done { + const event::encode *request = nullptr; + int32_t frame_count = 0; + int32_t width = 0; + uint64_t digest = 0; +}; + +struct encode_error { + const event::encode *request = nullptr; + emel::error::type err = {}; +}; + +} // namespace emel::speech::encoder::events diff --git a/src/emel/speech/encoder/whisper/detail.hpp b/src/emel/speech/encoder/whisper/detail.hpp index 17b0c14b..b2002dec 100644 --- a/src/emel/speech/encoder/whisper/detail.hpp +++ b/src/emel/speech/encoder/whisper/detail.hpp @@ -18,6 +18,7 @@ #include "emel/kernel/events.hpp" #include "emel/kernel/sm.hpp" #include "emel/model/data.hpp" +#include "emel/speech/encoder/events.hpp" #include "emel/speech/encoder/whisper/errors.hpp" namespace emel::speech::encoder::whisper::detail { @@ -44,15 +45,7 @@ inline constexpr float k_pi = 3.14159265358979323846f; inline constexpr float k_layer_norm_epsilon = 1.0e-5f; inline constexpr float k_log_zero_guard = 1.0e-10f; -struct execution_contract { - const emel::model::data *model = nullptr; - int32_t sample_rate = 0; - int32_t mel_bin_count = 0; - int32_t embedding_length = 0; - int32_t feed_forward_length = 0; - int32_t attention_head_count = 0; - int32_t encoder_block_count = 0; -}; +using execution_contract = emel::speech::encoder::execution_contract; inline execution_contract bind_execution_contract(const emel::model::data &model) noexcept { diff --git a/src/emel/speech/encoder/whisper/events.hpp b/src/emel/speech/encoder/whisper/events.hpp index 7053b0d2..dc5d6cc3 100644 --- a/src/emel/speech/encoder/whisper/events.hpp +++ b/src/emel/speech/encoder/whisper/events.hpp @@ -1,51 +1,21 @@ #pragma once -#include -#include - -#include "emel/callback.hpp" #include "emel/error/error.hpp" +#include "emel/speech/encoder/events.hpp" #include "emel/speech/encoder/whisper/errors.hpp" -namespace emel::speech::encoder::whisper::detail { - -struct execution_contract; - -} // namespace emel::speech::encoder::whisper::detail - namespace emel::speech::encoder::whisper::events { -struct encode_done; -struct encode_error; +// The whisper variant consumes the component-level event vocabulary directly; +// the component contract carries the variant-bound dimensions. +using encode_done = emel::speech::encoder::events::encode_done; +using encode_error = emel::speech::encoder::events::encode_error; } // namespace emel::speech::encoder::whisper::events namespace emel::speech::encoder::whisper::event { -struct encode { - encode(const detail::execution_contract &contract_ref, - std::span pcm_ref, const int32_t sample_rate_ref, - const int32_t channel_count_ref, std::span workspace_ref, - std::span encoder_state_ref, int32_t &frame_count_out_ref, - int32_t &width_out_ref, uint64_t &digest_out_ref) noexcept - : contract(contract_ref), pcm(pcm_ref), sample_rate(sample_rate_ref), - channel_count(channel_count_ref), workspace(workspace_ref), - encoder_state(encoder_state_ref), frame_count_out(frame_count_out_ref), - width_out(width_out_ref), digest_out(digest_out_ref) {} - - const detail::execution_contract &contract; - std::span pcm = {}; - int32_t sample_rate = 0; - int32_t channel_count = 0; - std::span workspace = {}; - std::span encoder_state = {}; - int32_t &frame_count_out; - int32_t &width_out; - uint64_t &digest_out; - emel::error::type *error_out = nullptr; - emel::callback on_done = {}; - emel::callback on_error = {}; -}; +using encode = emel::speech::encoder::event::encode; struct encode_ctx { emel::error::type err = emel::error::cast(error::none); @@ -57,19 +27,3 @@ struct encode_run { }; } // namespace emel::speech::encoder::whisper::event - -namespace emel::speech::encoder::whisper::events { - -struct encode_done { - const event::encode *request = nullptr; - int32_t frame_count = 0; - int32_t width = 0; - uint64_t digest = 0; -}; - -struct encode_error { - const event::encode *request = nullptr; - emel::error::type err = emel::error::cast(error::none); -}; - -} // namespace emel::speech::encoder::whisper::events diff --git a/src/emel/speech/recognizer/context.hpp b/src/emel/speech/recognizer/context.hpp deleted file mode 100644 index 8660dd4f..00000000 --- a/src/emel/speech/recognizer/context.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace emel::speech::recognizer::action { - -struct context {}; - -} // namespace emel::speech::recognizer::action diff --git a/src/emel/speech/recognizer_routes/whisper/actions.hpp b/src/emel/speech/recognizer_routes/whisper/actions.hpp deleted file mode 100644 index 425e4beb..00000000 --- a/src/emel/speech/recognizer_routes/whisper/actions.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "emel/error/error.hpp" -#include "emel/speech/decoder/whisper/any.hpp" -#include "emel/speech/decoder/whisper/sm.hpp" -#include "emel/speech/encoder/whisper/any.hpp" -#include "emel/speech/encoder/whisper/sm.hpp" -#include "emel/speech/recognizer/actions.hpp" -#include "emel/speech/recognizer/errors.hpp" -#include "emel/speech/recognizer/events.hpp" -#include "emel/speech/tokenizer/whisper/any.hpp" - -namespace emel::speech::recognizer_routes::whisper::action { - -namespace recognizer = emel::speech::recognizer; -namespace whisper_decoder = emel::speech::decoder::whisper; -namespace whisper_encoder = emel::speech::encoder::whisper; -namespace whisper_tokenizer = emel::speech::tokenizer::whisper; - -struct effect_encode { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - auto contract = - whisper_encoder::bind_execution_contract(runtime_ev.request.model); - whisper_encoder::sm encoder{}; - whisper_encoder::event::encode encode_ev{ - contract, - runtime_ev.request.pcm, - runtime_ev.request.sample_rate, - runtime_ev.request.channel_count, - runtime_ev.request.storage.encoder_workspace, - runtime_ev.request.storage.encoder_state, - runtime_ev.ctx.encoder_frame_count, - runtime_ev.ctx.encoder_width, - runtime_ev.ctx.encoder_digest}; - runtime_ev.ctx.encoder_accepted = encoder.process_event(encode_ev); - runtime_ev.ctx.err = emel::error::cast(recognizer::error::none); - } -}; - -struct effect_decode { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - auto contract = - whisper_decoder::bind_execution_contract(runtime_ev.request.model); - const auto encoder_state_size = - static_cast(runtime_ev.ctx.encoder_frame_count) * - static_cast(contract.embedding_length); - whisper_decoder::sm decoder{}; - whisper_decoder::event::decode decode_ev{ - contract, - std::span{runtime_ev.request.storage.encoder_state.data(), - encoder_state_size}, - runtime_ev.ctx.encoder_frame_count, - whisper_tokenizer::tiny_asr_decode_policy(), - runtime_ev.request.storage.generated_tokens, - runtime_ev.ctx.generated_token_count, - runtime_ev.request.storage.decoder_workspace, - runtime_ev.request.storage.logits, - runtime_ev.ctx.selected_token, - runtime_ev.ctx.confidence, - runtime_ev.ctx.decoder_digest}; - runtime_ev.ctx.decoder_accepted = decoder.process_event(decode_ev); - runtime_ev.ctx.err = emel::error::cast(recognizer::error::none); - } -}; - -struct effect_detokenize { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - runtime_ev.ctx.transcript_size = - static_cast(whisper_tokenizer::decode_token_ids( - runtime_ev.request.tokenizer.model_json, - std::span{ - runtime_ev.request.storage.generated_tokens.data(), - static_cast(runtime_ev.ctx.generated_token_count), - }, - runtime_ev.request.transcript.data(), - static_cast(runtime_ev.request.transcript.size()))); - runtime_ev.ctx.err = emel::error::cast(recognizer::error::none); - } -}; - -} // namespace emel::speech::recognizer_routes::whisper::action diff --git a/src/emel/speech/recognizer_routes/whisper/any.hpp b/src/emel/speech/recognizer_routes/whisper/any.hpp deleted file mode 100644 index afa11314..00000000 --- a/src/emel/speech/recognizer_routes/whisper/any.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#include "emel/speech/recognizer_routes/whisper/actions.hpp" -#include "emel/speech/recognizer_routes/whisper/guards.hpp" - -namespace emel::speech::recognizer_routes::whisper { - -uint64_t required_encoder_workspace_floats(uint64_t sample_count) noexcept; -uint64_t required_encoder_state_floats(uint64_t sample_count) noexcept; -uint64_t required_decoder_workspace_floats(uint64_t sample_count) noexcept; -int32_t logits_size() noexcept; -int32_t max_generated_token_count() noexcept; - -struct route { - using guard_tokenizer_supported = guard::guard_tokenizer_supported; - using guard_model_supported = guard::guard_model_supported; - using guard_recognition_ready = guard::guard_recognition_ready; - using effect_encode = action::effect_encode; - using effect_decode = action::effect_decode; - using effect_detokenize = action::effect_detokenize; -}; - -} // namespace emel::speech::recognizer_routes::whisper diff --git a/src/emel/speech/recognizer_routes/whisper/detail.cpp b/src/emel/speech/recognizer_routes/whisper/detail.cpp deleted file mode 100644 index fbbb7ca8..00000000 --- a/src/emel/speech/recognizer_routes/whisper/detail.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "emel/speech/recognizer_routes/whisper/any.hpp" - -#include - -#include "emel/speech/decoder/whisper/any.hpp" -#include "emel/speech/encoder/whisper/any.hpp" - -namespace emel::speech::recognizer_routes::whisper { - -namespace { - -namespace whisper_decoder = emel::speech::decoder::whisper; -namespace whisper_encoder = emel::speech::encoder::whisper; - -} // namespace - -uint64_t -required_encoder_workspace_floats(const uint64_t sample_count) noexcept { - return whisper_encoder::required_workspace_floats(sample_count); -} - -uint64_t required_encoder_state_floats(const uint64_t sample_count) noexcept { - return whisper_encoder::required_encoder_output_floats(sample_count); -} - -uint64_t -required_decoder_workspace_floats(const uint64_t sample_count) noexcept { - const uint64_t mel_frames = - whisper_encoder::mel_frame_count_for_samples(sample_count); - const uint64_t encoder_frames = - whisper_encoder::encoder_frame_count_for_mel_frames(mel_frames); - return whisper_decoder::required_workspace_floats(encoder_frames); -} - -int32_t logits_size() noexcept { return whisper_decoder::vocab_size(); } - -int32_t max_generated_token_count() noexcept { - return whisper_decoder::max_generated_token_count(); -} - -} // namespace emel::speech::recognizer_routes::whisper diff --git a/src/emel/speech/recognizer_routes/whisper/guards.hpp b/src/emel/speech/recognizer_routes/whisper/guards.hpp deleted file mode 100644 index c96c0047..00000000 --- a/src/emel/speech/recognizer_routes/whisper/guards.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include -#include - -#include "emel/error/error.hpp" -#include "emel/model/data.hpp" -#include "emel/model/loader/errors.hpp" -#include "emel/model/whisper/any.hpp" -#include "emel/speech/decoder/whisper/any.hpp" -#include "emel/speech/encoder/whisper/any.hpp" -#include "emel/speech/recognizer/events.hpp" -#include "emel/speech/tokenizer/whisper/any.hpp" - -namespace emel::speech::recognizer_routes::whisper::guard { - -namespace recognizer = emel::speech::recognizer; -namespace whisper_decoder = emel::speech::decoder::whisper; -namespace whisper_encoder = emel::speech::encoder::whisper; -namespace whisper_tokenizer = emel::speech::tokenizer::whisper; - -struct guard_tokenizer_supported { - bool - operator()(const recognizer::event::tokenizer_assets &assets) const noexcept { - return assets.model_json.data() != nullptr && !assets.model_json.empty() && - assets.sha256 == whisper_tokenizer::tiny_tokenizer_sha256() && - whisper_tokenizer::validate_tiny_control_tokens(assets.model_json) && - whisper_tokenizer::is_tiny_asr_decode_policy_supported( - whisper_tokenizer::tiny_asr_decode_policy()); - } -}; - -struct guard_model_supported { - bool operator()(const emel::model::data &model) const noexcept { - emel::model::whisper::execution_contract contract = {}; - return emel::model::whisper::build_execution_contract(model, contract) == - emel::error::cast(emel::model::loader::error::none); - } -}; - -struct guard_recognition_ready { - bool operator()(const recognizer::event::recognize &request) const noexcept { - const auto encoder_contract = - whisper_encoder::bind_execution_contract(request.model); - const uint64_t encoder_state_size = - whisper_encoder::required_encoder_output_floats(request.pcm.size()); - const uint64_t encoder_frames = - encoder_state_size / - static_cast(encoder_contract.embedding_length); - return guard_model_supported{}(request.model) && - guard_tokenizer_supported{}(request.tokenizer) && - request.storage.encoder_workspace.data() != nullptr && - request.storage.encoder_workspace.size() >= - static_cast(whisper_encoder::required_workspace_floats( - request.pcm.size())) && - request.storage.encoder_state.data() != nullptr && - request.storage.encoder_state.size() >= - static_cast(encoder_state_size) && - request.storage.decoder_workspace.data() != nullptr && - request.storage.decoder_workspace.size() >= - static_cast(whisper_decoder::required_workspace_floats( - encoder_frames)) && - request.storage.logits.data() != nullptr && - request.storage.logits.size() >= - static_cast(whisper_decoder::vocab_size()) && - request.storage.generated_tokens.data() != nullptr && - !request.storage.generated_tokens.empty(); - } -}; - -} // namespace emel::speech::recognizer_routes::whisper::guard diff --git a/src/emel/speech/tokenizer/any.hpp b/src/emel/speech/tokenizer/any.hpp new file mode 100644 index 00000000..4ddcd2f2 --- /dev/null +++ b/src/emel/speech/tokenizer/any.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "emel/sm.hpp" +#include "emel/speech/tokenizer/events.hpp" +#include "emel/speech/tokenizer/whisper/sm.hpp" + +namespace emel::speech::tokenizer { + +// Component variant selector. `unsupported` is a sentinel used by owners (e.g. +// speech/transcriber dependencies) to model "no tokenizer injected"; owners +// must guard dispatch on a supported kind. sm_any clamps out-of-range kinds to +// the first variant, so constructing a facade with `unsupported` is safe as +// long as no event is dispatched to it. +enum class tokenizer_kind : uint8_t { + whisper = 0, + unsupported = 255, +}; + +class any { +public: + any() = default; + explicit any(const tokenizer_kind kind) : core_(kind) {} + + any(const any &) = delete; + any &operator=(const any &) = delete; + any(any &&) = delete; + any &operator=(any &&) = delete; + + ~any() = default; + + void set_kind(const tokenizer_kind kind) { core_.set_kind(kind); } + + tokenizer_kind kind() const noexcept { return core_.kind(); } + + bool process_event(const event::detokenize &ev) { + return core_.process_event(ev); + } + +private: + using sm_list = stateforward::sml::aux::type_list; + using event_list = stateforward::sml::aux::type_list; + + emel::sm_any core_{}; +}; + +} // namespace emel::speech::tokenizer diff --git a/src/emel/speech/tokenizer/events.hpp b/src/emel/speech/tokenizer/events.hpp new file mode 100644 index 00000000..2bb8095c --- /dev/null +++ b/src/emel/speech/tokenizer/events.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include + +#include "emel/callback.hpp" +#include "emel/error/error.hpp" + +namespace emel::speech::tokenizer { + +// Component decode-policy contract: variant-neutral ASR decode configuration +// bound at the variant boundary (e.g. +// tokenizer::whisper::tiny_asr_decode_policy). Pure data; the variant machines +// validate the bound values before use. +struct control_tokens { + int32_t eot = 0; + int32_t sot = 0; + int32_t language_en = 0; + int32_t translate = 0; + int32_t transcribe = 0; + int32_t no_speech = 0; + int32_t notimestamps = 0; + int32_t timestamp_begin = 0; + int32_t space = 0; +}; + +enum class language_role : uint8_t { + english = 0u, +}; + +enum class task_role : uint8_t { + transcribe = 0u, +}; + +enum class timestamp_mode : uint8_t { + timestamp_tokens = 0u, +}; + +struct asr_decode_policy { + control_tokens tokens = {}; + language_role language = language_role::english; + task_role task = task_role::transcribe; + timestamp_mode timestamps = timestamp_mode::timestamp_tokens; + bool suppress_translate = false; + std::array prompt_tokens = {}; +}; + +} // namespace emel::speech::tokenizer + +namespace emel::speech::tokenizer::events { + +struct detokenize_done; +struct detokenize_error; + +} // namespace emel::speech::tokenizer::events + +namespace emel::speech::tokenizer::event { + +struct detokenize { + detokenize(const std::string_view tokenizer_json_ref, + std::span token_ids_ref, + std::span transcript_ref, + int32_t &transcript_size_out_ref) noexcept + : tokenizer_json(tokenizer_json_ref), token_ids(token_ids_ref), + transcript(transcript_ref), + transcript_size_out(transcript_size_out_ref) {} + + std::string_view tokenizer_json = {}; + std::span token_ids = {}; + std::span transcript = {}; + int32_t &transcript_size_out; + emel::error::type *error_out = nullptr; + emel::callback on_done = {}; + emel::callback on_error = {}; +}; + +} // namespace emel::speech::tokenizer::event + +namespace emel::speech::tokenizer::events { + +struct detokenize_done { + const event::detokenize *request = nullptr; + int32_t transcript_size = 0; +}; + +struct detokenize_error { + const event::detokenize *request = nullptr; + emel::error::type err = {}; +}; + +} // namespace emel::speech::tokenizer::events diff --git a/src/emel/speech/tokenizer/whisper/actions.hpp b/src/emel/speech/tokenizer/whisper/actions.hpp new file mode 100644 index 00000000..a3a72044 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/actions.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include "emel/error/error.hpp" +#include "emel/speech/tokenizer/whisper/detail.hpp" +#include "emel/speech/tokenizer/whisper/events.hpp" + +namespace emel::speech::tokenizer::whisper::action { + +// The tokenizer machine holds no persistent actor-owned state. +struct context {}; + +struct effect_begin_detokenize { + void operator()(const event::detokenize_run &, context &) const noexcept {} +}; + +struct effect_detokenize { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.transcript_size = + static_cast(detail::decode_token_ids( + runtime_ev.request.tokenizer_json, runtime_ev.request.token_ids, + runtime_ev.request.transcript.data(), + static_cast(runtime_ev.request.transcript.size()))); + runtime_ev.request.transcript_size_out = runtime_ev.ctx.transcript_size; + runtime_ev.ctx.err = emel::error::cast(error::none); + } +}; + +struct effect_mark_tokenizer_json_invalid { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::tokenizer_json_invalid); + } +}; + +struct effect_store_error_out { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + *runtime_ev.request.error_out = runtime_ev.ctx.err; + } +}; + +struct effect_emit_done { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.request.on_done(events::detokenize_done{ + .request = &runtime_ev.request, + .transcript_size = runtime_ev.ctx.transcript_size, + }); + } +}; + +struct effect_emit_error { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.request.on_error(events::detokenize_error{ + .request = &runtime_ev.request, + .err = runtime_ev.ctx.err, + }); + } +}; + +struct effect_on_unexpected { + template + void operator()(const unexpected_ev &, context &) const noexcept {} +}; + +} // namespace emel::speech::tokenizer::whisper::action diff --git a/src/emel/speech/tokenizer/whisper/detail.hpp b/src/emel/speech/tokenizer/whisper/detail.hpp index 31cfb9fa..e0f4b785 100644 --- a/src/emel/speech/tokenizer/whisper/detail.hpp +++ b/src/emel/speech/tokenizer/whisper/detail.hpp @@ -6,41 +6,26 @@ #include #include -namespace emel::speech::tokenizer::whisper::detail { - -struct control_tokens { - int32_t eot = 50257; - int32_t sot = 50258; - int32_t language_en = 50259; - int32_t translate = 50358; - int32_t transcribe = 50359; - int32_t no_speech = 50362; - int32_t notimestamps = 50363; - int32_t timestamp_begin = 50364; - int32_t space = 220; -}; - -inline constexpr control_tokens k_tiny_control_tokens{}; - -enum class language_role : uint8_t { - english = 0u, -}; - -enum class task_role : uint8_t { - transcribe = 0u, -}; +#include "emel/speech/tokenizer/events.hpp" -enum class timestamp_mode : uint8_t { - timestamp_tokens = 0u, -}; +namespace emel::speech::tokenizer::whisper::detail { -struct asr_decode_policy { - control_tokens tokens = {}; - language_role language = language_role::english; - task_role task = task_role::transcribe; - timestamp_mode timestamps = timestamp_mode::timestamp_tokens; - bool suppress_translate = true; - std::array prompt_tokens = {}; +using control_tokens = emel::speech::tokenizer::control_tokens; +using language_role = emel::speech::tokenizer::language_role; +using task_role = emel::speech::tokenizer::task_role; +using timestamp_mode = emel::speech::tokenizer::timestamp_mode; +using asr_decode_policy = emel::speech::tokenizer::asr_decode_policy; + +inline constexpr control_tokens k_tiny_control_tokens{ + .eot = 50257, + .sot = 50258, + .language_en = 50259, + .translate = 50358, + .transcribe = 50359, + .no_speech = 50362, + .notimestamps = 50363, + .timestamp_begin = 50364, + .space = 220, }; inline constexpr asr_decode_policy k_tiny_asr_decode_policy{ @@ -107,16 +92,14 @@ inline bool is_json_space(const char value) noexcept { return value == ' ' || value == '\n' || value == '\r' || value == '\t'; } -inline size_t skip_json_space(std::string_view text, - size_t offset) noexcept { +inline size_t skip_json_space(std::string_view text, size_t offset) noexcept { while (offset < text.size() && is_json_space(text[offset])) { ++offset; } return offset; } -inline bool find_json_object_value(std::string_view json, - std::string_view key, +inline bool find_json_object_value(std::string_view json, std::string_view key, std::string_view &object_out) noexcept { size_t search = 0u; while (search < json.size()) { @@ -169,8 +152,8 @@ inline bool find_json_object_value(std::string_view json, return false; } -inline std::string_view vocab_lookup_scope(std::string_view tokenizer_json) - noexcept { +inline std::string_view +vocab_lookup_scope(std::string_view tokenizer_json) noexcept { std::string_view vocab_json = {}; if (find_json_object_value(tokenizer_json, "\"vocab\"", vocab_json)) { return vocab_json; @@ -248,14 +231,14 @@ inline bool find_vocab_token_text(std::string_view tokenizer_json, token_text_out = token_text; return true; } - search = value_begin < vocab_json.size() ? value_begin + 1u - : vocab_json.size(); + search = + value_begin < vocab_json.size() ? value_begin + 1u : vocab_json.size(); } return false; } -inline size_t find_max_vocab_token_bytes(std::string_view tokenizer_json) - noexcept { +inline size_t +find_max_vocab_token_bytes(std::string_view tokenizer_json) noexcept { const std::string_view vocab_json = vocab_lookup_scope(tokenizer_json); size_t search = 0u; size_t max_token_bytes = 0u; @@ -269,8 +252,8 @@ inline size_t find_max_vocab_token_bytes(std::string_view tokenizer_json) if (token_text.size() > max_token_bytes) { max_token_bytes = token_text.size(); } - search = value_begin < vocab_json.size() ? value_begin + 1u - : vocab_json.size(); + search = + value_begin < vocab_json.size() ? value_begin + 1u : vocab_json.size(); } return max_token_bytes; } @@ -335,8 +318,8 @@ inline void append_decoded_piece(std::string_view piece, char *transcript, decode_json_escaped_byte(piece[index + 1u], out)) { advance = 2u; } else if (index + 1u < piece.size() && - static_cast(piece[index]) == 0xc4u && - static_cast(piece[index + 1u]) == 0xa0u) { + static_cast(piece[index]) == 0xc4u && + static_cast(piece[index + 1u]) == 0xa0u) { out = ' '; advance = 2u; } diff --git a/src/emel/speech/tokenizer/whisper/errors.hpp b/src/emel/speech/tokenizer/whisper/errors.hpp new file mode 100644 index 00000000..ec8cf537 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/errors.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "emel/error/error.hpp" + +namespace emel::speech::tokenizer::whisper { + +enum class error : emel::error::type { + none = 0, + tokenizer_json_invalid = 1, + internal_error = 2, +}; + +} // namespace emel::speech::tokenizer::whisper diff --git a/src/emel/speech/tokenizer/whisper/events.hpp b/src/emel/speech/tokenizer/whisper/events.hpp new file mode 100644 index 00000000..86244b88 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/events.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "emel/error/error.hpp" +#include "emel/speech/tokenizer/events.hpp" +#include "emel/speech/tokenizer/whisper/errors.hpp" + +namespace emel::speech::tokenizer::whisper::events { + +// The whisper variant consumes the component-level event vocabulary directly. +using detokenize_done = emel::speech::tokenizer::events::detokenize_done; +using detokenize_error = emel::speech::tokenizer::events::detokenize_error; + +} // namespace emel::speech::tokenizer::whisper::events + +namespace emel::speech::tokenizer::whisper::event { + +using detokenize = emel::speech::tokenizer::event::detokenize; + +struct detokenize_ctx { + emel::error::type err = emel::error::cast(error::none); + int32_t transcript_size = 0; +}; + +struct detokenize_run { + const detokenize &request; + detokenize_ctx &ctx; +}; + +} // namespace emel::speech::tokenizer::whisper::event diff --git a/src/emel/speech/tokenizer/whisper/guards.hpp b/src/emel/speech/tokenizer/whisper/guards.hpp new file mode 100644 index 00000000..c14442cb --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/guards.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "emel/speech/tokenizer/whisper/detail.hpp" +#include "emel/speech/tokenizer/whisper/events.hpp" + +namespace emel::speech::tokenizer::whisper::guard { + +struct guard_tokenizer_json_valid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return runtime_ev.request.tokenizer_json.data() != nullptr && + !runtime_ev.request.tokenizer_json.empty() && + detail::validate_tiny_control_tokens( + runtime_ev.request.tokenizer_json); + } +}; + +struct guard_tokenizer_json_invalid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return !guard_tokenizer_json_valid{}(runtime_ev); + } +}; + +struct guard_has_error_out { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return runtime_ev.request.error_out != nullptr; + } +}; + +struct guard_no_error_out { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return runtime_ev.request.error_out == nullptr; + } +}; + +struct guard_has_done_callback { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return static_cast(runtime_ev.request.on_done); + } +}; + +struct guard_no_done_callback { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return !static_cast(runtime_ev.request.on_done); + } +}; + +struct guard_has_error_callback { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return static_cast(runtime_ev.request.on_error); + } +}; + +struct guard_no_error_callback { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return !static_cast(runtime_ev.request.on_error); + } +}; + +} // namespace emel::speech::tokenizer::whisper::guard diff --git a/src/emel/speech/tokenizer/whisper/sm.hpp b/src/emel/speech/tokenizer/whisper/sm.hpp new file mode 100644 index 00000000..cddd9d40 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/sm.hpp @@ -0,0 +1,131 @@ +#pragma once +// benchmark: designed + +#include + +#include "emel/sm.hpp" +#include "emel/speech/tokenizer/whisper/actions.hpp" +#include "emel/speech/tokenizer/whisper/events.hpp" +#include "emel/speech/tokenizer/whisper/guards.hpp" + +namespace emel::speech::tokenizer::whisper { + +struct state_ready {}; +struct state_json_decision {}; +struct state_detokenizing {}; +struct state_success_error_out_decision {}; +struct state_success_callback_decision {}; +struct state_error_error_out_decision {}; +struct state_error_callback_decision {}; +struct state_done {}; +struct state_errored {}; + +/* +whisper detokenize machine + +state purpose +- state_json_decision validates the request before any transcript bytes are + written: the tokenizer JSON must carry the tiny control tokens. +- state_detokenizing performs the bounded token-id -> text decode in a single + transition (data-plane iteration only; no behavior selection). The decode + truncates to the caller's transcript capacity, matching the public + transcriber contract. +- success/error error-out and callback decision states mirror the encoder and + decoder machines so error_out and on_done/on_error channels are explicit + transitions, never action-side branching. +- state_done / state_errored return to state_ready by completion, so one + machine instance serves repeated detokenize dispatches. + +invariants +- guards are pure predicates of the dispatch-run event; all context mutation + happens in actions. +- the machine holds no persistent context; every dispatch is self-contained. +*/ +struct model { + auto operator()() const { + namespace sml = stateforward::sml; + + // clang-format off + return sml::make_transition_table( + //------------------------------------------------------------------------------// + // Request validation. + sml::state <= *sml::state + + sml::event + / action::effect_begin_detokenize{} + , sml::state <= sml::state + + sml::completion [ guard::guard_tokenizer_json_valid{} ] + / action::effect_detokenize{} + , sml::state <= sml::state + + sml::completion [ guard::guard_tokenizer_json_invalid{} ] + / action::effect_mark_tokenizer_json_invalid{} + + //------------------------------------------------------------------------------// + // Publish. + , sml::state <= sml::state + + sml::completion + , sml::state <= sml::state + + sml::completion [ guard::guard_has_error_out{} ] + / action::effect_store_error_out{} + , sml::state <= sml::state + + sml::completion [ guard::guard_no_error_out{} ] + , sml::state <= sml::state + + sml::completion [ guard::guard_has_error_out{} ] + / action::effect_store_error_out{} + , sml::state <= sml::state + + sml::completion [ guard::guard_no_error_out{} ] + , sml::state <= sml::state + + sml::completion [ guard::guard_has_done_callback{} ] + / action::effect_emit_done{} + , sml::state <= sml::state + + sml::completion [ guard::guard_no_done_callback{} ] + , sml::state <= sml::state + + sml::completion [ guard::guard_has_error_callback{} ] + / action::effect_emit_error{} + , sml::state <= sml::state + + sml::completion [ guard::guard_no_error_callback{} ] + , sml::state <= sml::state + + sml::completion + , sml::state <= sml::state + + sml::completion + + //------------------------------------------------------------------------------// + // Unexpected events. + , sml::state <= sml::state + sml::unexpected_event + / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + ); + // clang-format on + } +}; + +struct sm : public emel::sm { + using base_type = emel::sm; + using base_type::is; + using base_type::visit_current_states; + + sm() = default; + + bool process_event(const event::detokenize &ev) { + event::detokenize_ctx ctx{}; + event::detokenize_run runtime_ev{ev, ctx}; + const bool accepted = base_type::process_event(runtime_ev); + return accepted && ctx.err == emel::error::cast(error::none); + } +}; + +} // namespace emel::speech::tokenizer::whisper diff --git a/src/emel/speech/recognizer/actions.hpp b/src/emel/speech/transcriber/actions.hpp similarity index 71% rename from src/emel/speech/recognizer/actions.hpp rename to src/emel/speech/transcriber/actions.hpp index afa50663..60d3ce69 100644 --- a/src/emel/speech/recognizer/actions.hpp +++ b/src/emel/speech/transcriber/actions.hpp @@ -1,10 +1,14 @@ #pragma once -#include "emel/speech/recognizer/context.hpp" -#include "emel/speech/recognizer/detail.hpp" -#include "emel/speech/recognizer/events.hpp" +#include +#include +#include -namespace emel::speech::recognizer::action { +#include "emel/speech/transcriber/context.hpp" +#include "emel/speech/transcriber/detail.hpp" +#include "emel/speech/transcriber/events.hpp" + +namespace emel::speech::transcriber::action { struct effect_begin_initialize { void operator()(const event::initialize_run &runtime_ev, @@ -76,6 +80,7 @@ struct effect_begin_recognize { runtime_ev.ctx.err = detail::to_error(error::none); runtime_ev.ctx.encoder_accepted = false; runtime_ev.ctx.decoder_accepted = false; + runtime_ev.ctx.detokenize_accepted = false; runtime_ev.ctx.encoder_frame_count = 0; runtime_ev.ctx.encoder_width = 0; runtime_ev.ctx.generated_token_count = 0; @@ -112,6 +117,68 @@ struct effect_mark_backend_error { } }; +// Pipeline effects: dispatch into the injected component actors that the sm +// wrapper owns. Each effect drives one already-chosen phase; which phase runs +// (and whether the pipeline runs at all) is decided by guards in the transition +// table, never here. +struct effect_encode { + void operator()(const event::recognize_run &runtime_ev, + context &ctx) const noexcept { + speech::encoder::event::encode encode_ev{ + ctx.deps.encoder_contract, + runtime_ev.request.pcm, + runtime_ev.request.sample_rate, + runtime_ev.request.channel_count, + runtime_ev.request.storage.encoder_workspace, + runtime_ev.request.storage.encoder_state, + runtime_ev.ctx.encoder_frame_count, + runtime_ev.ctx.encoder_width, + runtime_ev.ctx.encoder_digest}; + runtime_ev.ctx.encoder_accepted = ctx.encoder->process_event(encode_ev); + runtime_ev.ctx.err = detail::to_error(error::none); + } +}; + +struct effect_decode { + void operator()(const event::recognize_run &runtime_ev, + context &ctx) const noexcept { + const auto encoder_state_size = + static_cast(runtime_ev.ctx.encoder_frame_count) * + static_cast(ctx.deps.decoder_contract.embedding_length); + speech::decoder::event::decode decode_ev{ + ctx.deps.decoder_contract, + std::span{runtime_ev.request.storage.encoder_state.data(), + encoder_state_size}, + runtime_ev.ctx.encoder_frame_count, + ctx.deps.decode_policy, + runtime_ev.request.storage.generated_tokens, + runtime_ev.ctx.generated_token_count, + runtime_ev.request.storage.decoder_workspace, + runtime_ev.request.storage.logits, + runtime_ev.ctx.selected_token, + runtime_ev.ctx.confidence, + runtime_ev.ctx.decoder_digest}; + runtime_ev.ctx.decoder_accepted = ctx.decoder->process_event(decode_ev); + runtime_ev.ctx.err = detail::to_error(error::none); + } +}; + +struct effect_detokenize { + void operator()(const event::recognize_run &runtime_ev, + context &ctx) const noexcept { + speech::tokenizer::event::detokenize detokenize_ev{ + runtime_ev.request.tokenizer.model_json, + std::span{ + runtime_ev.request.storage.generated_tokens.data(), + static_cast(runtime_ev.ctx.generated_token_count), + }, + runtime_ev.request.transcript, runtime_ev.ctx.transcript_size}; + runtime_ev.ctx.detokenize_accepted = + ctx.tokenizer->process_event(detokenize_ev); + runtime_ev.ctx.err = detail::to_error(error::none); + } +}; + struct effect_publish_recognition_outputs { void operator()(const event::recognize_run &runtime_ev, context &) const noexcept { @@ -184,6 +251,9 @@ inline constexpr effect_store_initialize_success inline constexpr effect_emit_initialize_done effect_emit_initialize_done{}; inline constexpr effect_emit_initialize_error effect_emit_initialize_error{}; inline constexpr effect_begin_recognize effect_begin_recognize{}; +inline constexpr effect_encode effect_encode{}; +inline constexpr effect_decode effect_decode{}; +inline constexpr effect_detokenize effect_detokenize{}; inline constexpr effect_reject_recognize effect_reject_recognize{}; inline constexpr effect_mark_uninitialized effect_mark_uninitialized{}; inline constexpr effect_mark_backend_error effect_mark_backend_error{}; @@ -196,29 +266,4 @@ inline constexpr effect_emit_recognize_done effect_emit_recognize_done{}; inline constexpr effect_emit_recognize_error effect_emit_recognize_error{}; inline constexpr effect_on_unexpected effect_on_unexpected{}; -} // namespace emel::speech::recognizer::action - -namespace emel::speech::recognizer::route { - -struct effect_encode_unsupported { - void operator()(const event::recognize_run &runtime_ev, - action::context &) const noexcept { - runtime_ev.ctx.err = detail::to_error(error::backend); - } -}; - -struct effect_decode_unsupported { - void operator()(const event::recognize_run &runtime_ev, - action::context &) const noexcept { - runtime_ev.ctx.err = detail::to_error(error::backend); - } -}; - -struct effect_detokenize_unsupported { - void operator()(const event::recognize_run &runtime_ev, - action::context &) const noexcept { - runtime_ev.ctx.err = detail::to_error(error::backend); - } -}; - -} // namespace emel::speech::recognizer::route +} // namespace emel::speech::transcriber::action diff --git a/src/emel/speech/transcriber/context.hpp b/src/emel/speech/transcriber/context.hpp new file mode 100644 index 00000000..04b594fc --- /dev/null +++ b/src/emel/speech/transcriber/context.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "emel/speech/decoder/any.hpp" +#include "emel/speech/encoder/any.hpp" +#include "emel/speech/tokenizer/any.hpp" + +namespace emel::speech::transcriber { + +// Injected component contracts (text/generator dependencies pattern): pure data +// bound at the variant boundary after model binding. The transcriber engine has +// no model-family knowledge; which variant runs is decided by the kinds, and +// how it runs is decided by the contracts and decode policy the caller injects. +// The default value models "no components injected" and initialize rejects it. +struct dependencies { + speech::encoder::encoder_kind encoder_kind = + speech::encoder::encoder_kind::unsupported; + speech::decoder::decoder_kind decoder_kind = + speech::decoder::decoder_kind::unsupported; + speech::tokenizer::tokenizer_kind tokenizer_kind = + speech::tokenizer::tokenizer_kind::unsupported; + speech::encoder::execution_contract encoder_contract = {}; + speech::decoder::execution_contract decoder_contract = {}; + speech::tokenizer::asr_decode_policy decode_policy = {}; + std::string_view tokenizer_sha256 = {}; +}; + +} // namespace emel::speech::transcriber + +namespace emel::speech::transcriber::action { + +// Persistent actor-owned state: the injected dependencies and the component +// actors the sm wrapper owns for the machine's lifetime. The pointers are wired +// once at construction, before any process_event dispatch. +struct context { + dependencies deps = {}; + speech::encoder::any *encoder = nullptr; + speech::decoder::any *decoder = nullptr; + speech::tokenizer::any *tokenizer = nullptr; +}; + +} // namespace emel::speech::transcriber::action diff --git a/src/emel/speech/recognizer/detail.hpp b/src/emel/speech/transcriber/detail.hpp similarity index 50% rename from src/emel/speech/recognizer/detail.hpp rename to src/emel/speech/transcriber/detail.hpp index 85447010..675c2c26 100644 --- a/src/emel/speech/recognizer/detail.hpp +++ b/src/emel/speech/transcriber/detail.hpp @@ -1,12 +1,12 @@ #pragma once #include "emel/error/error.hpp" -#include "emel/speech/recognizer/errors.hpp" +#include "emel/speech/transcriber/errors.hpp" -namespace emel::speech::recognizer::detail { +namespace emel::speech::transcriber::detail { inline emel::error::type to_error(const error err) noexcept { return emel::error::cast(err); } -} // namespace emel::speech::recognizer::detail +} // namespace emel::speech::transcriber::detail diff --git a/src/emel/speech/recognizer/errors.hpp b/src/emel/speech/transcriber/errors.hpp similarity index 76% rename from src/emel/speech/recognizer/errors.hpp rename to src/emel/speech/transcriber/errors.hpp index bd433f13..3b61aa1b 100644 --- a/src/emel/speech/recognizer/errors.hpp +++ b/src/emel/speech/transcriber/errors.hpp @@ -2,7 +2,7 @@ #include "emel/error/error.hpp" -namespace emel::speech::recognizer { +namespace emel::speech::transcriber { enum class error : emel::error::type { none = 0, @@ -15,4 +15,4 @@ enum class error : emel::error::type { output_capacity = 7, }; -} // namespace emel::speech::recognizer +} // namespace emel::speech::transcriber diff --git a/src/emel/speech/recognizer/events.hpp b/src/emel/speech/transcriber/events.hpp similarity index 95% rename from src/emel/speech/recognizer/events.hpp rename to src/emel/speech/transcriber/events.hpp index 3127b328..5dfe4d60 100644 --- a/src/emel/speech/recognizer/events.hpp +++ b/src/emel/speech/transcriber/events.hpp @@ -7,18 +7,18 @@ #include "emel/callback.hpp" #include "emel/error/error.hpp" #include "emel/model/data.hpp" -#include "emel/speech/recognizer/errors.hpp" +#include "emel/speech/transcriber/errors.hpp" -namespace emel::speech::recognizer::events { +namespace emel::speech::transcriber::events { struct initialize_done; struct initialize_error; struct recognition_done; struct recognition_error; -} // namespace emel::speech::recognizer::events +} // namespace emel::speech::transcriber::events -namespace emel::speech::recognizer::event { +namespace emel::speech::transcriber::event { struct tokenizer_assets { std::string_view model_json = {}; @@ -169,6 +169,7 @@ struct recognize_ctx { emel::error::type err = emel::error::cast(error::none); bool encoder_accepted = false; bool decoder_accepted = false; + bool detokenize_accepted = false; int32_t encoder_frame_count = 0; int32_t encoder_width = 0; int32_t generated_token_count = 0; @@ -184,9 +185,9 @@ struct recognize_run { recognize_ctx &ctx; }; -} // namespace emel::speech::recognizer::event +} // namespace emel::speech::transcriber::event -namespace emel::speech::recognizer::events { +namespace emel::speech::transcriber::events { struct initialize_done { const event::initialize *request = nullptr; @@ -214,4 +215,4 @@ struct recognition_error { emel::error::type err = emel::error::cast(error::none); }; -} // namespace emel::speech::recognizer::events +} // namespace emel::speech::transcriber::events diff --git a/src/emel/speech/recognizer/guards.hpp b/src/emel/speech/transcriber/guards.hpp similarity index 63% rename from src/emel/speech/recognizer/guards.hpp rename to src/emel/speech/transcriber/guards.hpp index 3bf92bc2..0e7c73e8 100644 --- a/src/emel/speech/recognizer/guards.hpp +++ b/src/emel/speech/transcriber/guards.hpp @@ -1,10 +1,10 @@ #pragma once -#include "emel/speech/recognizer/context.hpp" -#include "emel/speech/recognizer/detail.hpp" -#include "emel/speech/recognizer/events.hpp" +#include "emel/speech/transcriber/context.hpp" +#include "emel/speech/transcriber/detail.hpp" +#include "emel/speech/transcriber/events.hpp" -namespace emel::speech::recognizer::guard { +namespace emel::speech::transcriber::guard { struct guard_valid_initialize { bool operator()(const event::initialize_run &runtime_ev, @@ -21,42 +21,63 @@ struct guard_invalid_initialize { } }; -template struct guard_initialize_tokenizer_supported { - bool operator()(const event::initialize_run &runtime_ev, - const action::context &) const noexcept { - return typename route_policy::guard_tokenizer_supported{}( - runtime_ev.request.tokenizer); +// Injected-dependency support predicates: the engine accepts a request only +// when the caller injected supported component kinds, the component contracts +// were bound against the same model the event carries, and the tokenizer assets +// match the pinned checksum in the dependencies. Content-level validation +// (model tensors, workspace capacity, tokenizer JSON control tokens) is owned +// by the component machines and re-checked on every component dispatch. +struct guard_tokenizer_assets_supported { + bool operator()(const event::tokenizer_assets &assets, + const dependencies &deps) const noexcept { + return deps.tokenizer_kind != + speech::tokenizer::tokenizer_kind::unsupported && + assets.model_json.data() != nullptr && !assets.model_json.empty() && + !deps.tokenizer_sha256.empty() && + assets.sha256 == deps.tokenizer_sha256; + } +}; + +struct guard_model_contracts_supported { + bool operator()(const emel::model::data &model, + const dependencies &deps) const noexcept { + return deps.encoder_kind != speech::encoder::encoder_kind::unsupported && + deps.decoder_kind != speech::decoder::decoder_kind::unsupported && + deps.encoder_contract.model == &model && + deps.encoder_contract.embedding_length > 0 && + deps.decoder_contract.model == &model && + deps.decoder_contract.vocab_size > 0 && + deps.decoder_contract.embedding_length > 0; } }; -template struct guard_initialize_tokenizer_unsupported { +struct guard_initialize_tokenizer_supported { bool operator()(const event::initialize_run &runtime_ev, const action::context &ctx) const noexcept { - return !guard_initialize_tokenizer_supported{}(runtime_ev, - ctx); + return guard_tokenizer_assets_supported{}(runtime_ev.request.tokenizer, + ctx.deps); } }; -template struct guard_initialize_model_supported { +struct guard_initialize_tokenizer_unsupported { bool operator()(const event::initialize_run &runtime_ev, - const action::context &) const noexcept { - return typename route_policy::guard_model_supported{}( - runtime_ev.request.model); + const action::context &ctx) const noexcept { + return !guard_initialize_tokenizer_supported{}(runtime_ev, ctx); } }; -template -struct guard_initialize_model_supported_and_route_storage_ready { +struct guard_initialize_model_supported { bool operator()(const event::initialize_run &runtime_ev, const action::context &ctx) const noexcept { - return guard_initialize_model_supported{}(runtime_ev, ctx); + return guard_model_contracts_supported{}(runtime_ev.request.model, + ctx.deps); } }; -template struct guard_initialize_unsupported_model { +struct guard_initialize_unsupported_model { bool operator()(const event::initialize_run &runtime_ev, const action::context &ctx) const noexcept { - return !guard_initialize_model_supported{}(runtime_ev, ctx); + return !guard_initialize_model_supported{}(runtime_ev, ctx); } }; @@ -119,17 +140,20 @@ struct guard_invalid_recognize { } }; -template struct guard_recognizer_route_ready { +struct guard_transcriber_ready { bool operator()(const event::recognize_run &runtime_ev, - const action::context &) const noexcept { - return typename route_policy::guard_recognition_ready{}(runtime_ev.request); + const action::context &ctx) const noexcept { + return guard_model_contracts_supported{}(runtime_ev.request.model, + ctx.deps) && + guard_tokenizer_assets_supported{}(runtime_ev.request.tokenizer, + ctx.deps); } }; -template struct guard_recognizer_route_unsupported { +struct guard_transcriber_unsupported { bool operator()(const event::recognize_run &runtime_ev, const action::context &ctx) const noexcept { - return !guard_recognizer_route_ready{}(runtime_ev, ctx); + return !guard_transcriber_ready{}(runtime_ev, ctx); } }; @@ -163,6 +187,21 @@ struct guard_decoder_failure { } }; +struct guard_detokenize_success { + bool operator()(const event::recognize_run &runtime_ev, + const action::context &) const noexcept { + return runtime_ev.ctx.detokenize_accepted && + runtime_ev.ctx.err == detail::to_error(error::none); + } +}; + +struct guard_detokenize_failure { + bool operator()(const event::recognize_run &runtime_ev, + const action::context &ctx) const noexcept { + return !guard_detokenize_success{}(runtime_ev, ctx); + } +}; + struct guard_has_recognize_done_callback { bool operator()(const event::recognize_run &runtime_ev, const action::context &) const noexcept { @@ -205,23 +244,4 @@ struct guard_no_recognize_error_out { } }; -} // namespace emel::speech::recognizer::guard - -namespace emel::speech::recognizer::route { - -struct guard_unsupported_tokenizer { - bool operator()(const event::tokenizer_assets &assets) const noexcept { - return assets.model_json.data() != nullptr && !assets.model_json.empty() && - assets.sha256.data() != nullptr && !assets.sha256.empty(); - } -}; - -struct guard_unsupported_model { - bool operator()(const emel::model::data &) const noexcept { return false; } -}; - -struct guard_unsupported_recognition { - bool operator()(const event::recognize &) const noexcept { return false; } -}; - -} // namespace emel::speech::recognizer::route +} // namespace emel::speech::transcriber::guard diff --git a/src/emel/speech/recognizer/sm.hpp b/src/emel/speech/transcriber/sm.hpp similarity index 69% rename from src/emel/speech/recognizer/sm.hpp rename to src/emel/speech/transcriber/sm.hpp index 1e76b429..ebc8b550 100644 --- a/src/emel/speech/recognizer/sm.hpp +++ b/src/emel/speech/transcriber/sm.hpp @@ -4,29 +4,29 @@ #include #include "emel/sm.hpp" -#include "emel/speech/recognizer/actions.hpp" -#include "emel/speech/recognizer/context.hpp" -#include "emel/speech/recognizer/events.hpp" -#include "emel/speech/recognizer/guards.hpp" +#include "emel/speech/transcriber/actions.hpp" +#include "emel/speech/transcriber/context.hpp" +#include "emel/speech/transcriber/events.hpp" +#include "emel/speech/transcriber/guards.hpp" -namespace emel::speech::recognizer { +namespace emel::speech::transcriber { struct state_uninitialized {}; struct state_initializing {}; struct state_tokenizer_decision {}; -struct state_model_route_decision {}; +struct state_model_support_decision {}; struct state_initialize_success {}; struct state_initialize_error_out_decision {}; struct state_initialize_done_callback_decision {}; struct state_initialize_error_callback_decision {}; struct state_ready {}; -struct state_recognize_route_decision {}; -struct state_recognize_preparing {}; -struct state_route_encoding {}; -struct state_route_encoder_decision {}; -struct state_route_decoding {}; -struct state_route_decoder_decision {}; -struct state_route_detokenizing {}; +struct state_recognize_support_decision {}; +struct state_encoding {}; +struct state_encoder_decision {}; +struct state_decoding {}; +struct state_decoder_decision {}; +struct state_detokenizing {}; +struct state_detokenize_decision {}; struct state_recognize_success {}; struct state_recognize_error_out_decision {}; struct state_recognize_done_callback_decision {}; @@ -34,20 +34,29 @@ struct state_recognize_error_callback_decision {}; struct state_done {}; struct state_errored {}; -namespace route { +/* +speech transcriber engine (single source of truth) -struct unsupported { - using guard_tokenizer_supported = guard_unsupported_tokenizer; - using guard_model_supported = guard_unsupported_model; - using guard_recognition_ready = guard_unsupported_recognition; - using effect_encode = effect_encode_unsupported; - using effect_decode = effect_decode_unsupported; - using effect_detokenize = effect_detokenize_unsupported; -}; - -} // namespace route +state purpose +- initialize validates the injected dependencies against the request: the + tokenizer assets must match the pinned checksum and the component contracts + must have been bound against the same model the event carries. +- recognize drives the encode -> decode -> detokenize pipeline by dispatching + into the injected component actors (speech/encoder, speech/decoder, + speech/tokenizer facades); each phase outcome is an explicit decision state. +- done/error error-out and callback decision states keep the error_out and + on_done/on_error channels as explicit transitions. -template struct model { +control invariants +- the machine contains no model-family names, contracts, routes, constants, or + variant behavior; which variant runs is decided entirely by the injected + dependencies (component kinds + contracts + decode policy), and content-level + validation is owned by the component machines themselves. +- guards are pure predicates of (event, context); all mutation happens in + actions; component dispatch is synchronous, acyclic, and joined before the + action returns. +*/ +struct model { auto operator()() const { namespace sml = stateforward::sml; @@ -73,19 +82,19 @@ template struct model { , sml::state <= sml::state + sml::completion - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion - [ guard::guard_initialize_tokenizer_supported{} ] + [ guard::guard_initialize_tokenizer_supported{} ] , sml::state <= sml::state + sml::completion - [ guard::guard_initialize_tokenizer_unsupported{} ] + [ guard::guard_initialize_tokenizer_unsupported{} ] / action::effect_mark_tokenizer_invalid - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion - [ guard::guard_initialize_model_supported_and_route_storage_ready{} ] - , sml::state <= sml::state + [ guard::guard_initialize_model_supported{} ] + , sml::state <= sml::state + sml::completion - [ guard::guard_initialize_unsupported_model{} ] + [ guard::guard_initialize_unsupported_model{} ] / action::effect_mark_unsupported_model , sml::state <= sml::state @@ -113,7 +122,7 @@ template struct model { //------------------------------------------------------------------------------// // Recognition. - , sml::state <= sml::state + , sml::state <= sml::state + sml::event [ guard::guard_valid_recognize{} ] / action::effect_begin_recognize , sml::state <= sml::state @@ -127,35 +136,41 @@ template struct model { / action::effect_mark_uninitialized , sml::state <= - sml::state + sml::state + sml::completion - [ guard::guard_recognizer_route_unsupported{} ] + [ guard::guard_transcriber_unsupported{} ] / action::effect_mark_uninitialized - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion - [ guard::guard_recognizer_route_ready{} ] - / typename route_policy::effect_encode{} - , sml::state <= sml::state + [ guard::guard_transcriber_ready{} ] + / action::effect_encode + , sml::state <= sml::state + sml::completion - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion [ guard::guard_encoder_success{} ] - / typename route_policy::effect_decode{} + / action::effect_decode , sml::state <= - sml::state + sml::state + sml::completion [ guard::guard_encoder_failure{} ] / action::effect_mark_backend_error - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion [ guard::guard_decoder_success{} ] - / typename route_policy::effect_detokenize{} + / action::effect_detokenize , sml::state <= - sml::state + sml::state + sml::completion [ guard::guard_decoder_failure{} ] / action::effect_mark_backend_error - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion + , sml::state <= sml::state + + sml::completion [ guard::guard_detokenize_success{} ] / action::effect_publish_recognition_outputs + , sml::state <= + sml::state + + sml::completion [ guard::guard_detokenize_failure{} ] + / action::effect_mark_backend_error , sml::state <= sml::state @@ -197,13 +212,26 @@ template struct model { } }; -template -struct sm : public emel::sm, action::context> { - using base_type = emel::sm, action::context>; +struct sm : public emel::sm { + using base_type = emel::sm; using base_type::is; using base_type::visit_current_states; - sm() = default; + sm() : sm(dependencies{}) {} + + explicit sm(const dependencies &deps) + : base_type(), encoder_(deps.encoder_kind), decoder_(deps.decoder_kind), + tokenizer_(deps.tokenizer_kind) { + this->context_.deps = deps; + this->context_.encoder = &encoder_; + this->context_.decoder = &decoder_; + this->context_.tokenizer = &tokenizer_; + } + + sm(const sm &) = delete; + sm &operator=(const sm &) = delete; + sm(sm &&) = delete; + sm &operator=(sm &&) = delete; bool process_event(const event::initialize &ev) { event::initialize_ctx ctx{}; @@ -218,14 +246,19 @@ struct sm : public emel::sm, action::context> { const bool accepted = base_type::process_event(runtime_ev); return accepted && ctx.err == detail::to_error(error::none); } + +private: + speech::encoder::any encoder_; + speech::decoder::any decoder_; + speech::tokenizer::any tokenizer_; }; -using Recognizer = sm<>; +using Transcriber = sm; -} // namespace emel::speech::recognizer +} // namespace emel::speech::transcriber namespace emel { -using SpeechRecognizer = speech::recognizer::sm<>; +using SpeechTranscriber = speech::transcriber::sm; } // namespace emel diff --git a/tests/diarization/sortformer/pipeline/lifecycle_tests.cpp b/tests/diarization/sortformer/pipeline/lifecycle_tests.cpp index 6078373f..c63bdab8 100644 --- a/tests/diarization/sortformer/pipeline/lifecycle_tests.cpp +++ b/tests/diarization/sortformer/pipeline/lifecycle_tests.cpp @@ -223,9 +223,9 @@ struct synthetic_model_fixture { } }; -emel::model::sortformer::detail::execution_contract make_synthetic_contract( +emel::model::sortformer::execution_contract make_synthetic_contract( const emel::model::data & model) noexcept { - emel::model::sortformer::detail::execution_contract contract = {}; + emel::model::sortformer::execution_contract contract = {}; contract.model = &model; contract.sample_rate = pipeline_detail::k_sample_rate; contract.speaker_count = pipeline_detail::k_speaker_count; diff --git a/tests/model/moshi/binding_tests.cpp b/tests/model/moshi/binding_tests.cpp index e9708712..1eaeaf08 100644 --- a/tests/model/moshi/binding_tests.cpp +++ b/tests/model/moshi/binding_tests.cpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include #include #include @@ -373,12 +373,9 @@ uint64_t quant_storage_bytes(const int64_t cols, const int64_t rows, return blocks * block_bytes; } -void set_matrix_tensor(emel::model::data &model, - const std::string_view name, - const int32_t dtype, - const int64_t cols, - const int64_t rows, - const uint64_t data_size) { +void set_matrix_tensor(emel::model::data &model, const std::string_view name, + const int32_t dtype, const int64_t cols, + const int64_t rows, const uint64_t data_size) { auto *tensor = find_mutable_tensor(model, name); REQUIRE(tensor != nullptr); REQUIRE(cols > 0); @@ -389,12 +386,9 @@ void set_matrix_tensor(emel::model::data &model, tensor->data_size = data_size; } -void set_q4_k_matrix(emel::model::data &model, - const std::string_view name, - const int64_t cols, - const int64_t rows) { - REQUIRE(cols % static_cast(emel::kernel::detail::quant::QK_K) == - 0); +void set_q4_k_matrix(emel::model::data &model, const std::string_view name, + const int64_t cols, const int64_t rows) { + REQUIRE(cols % static_cast(emel::kernel::detail::quant::QK_K) == 0); set_matrix_tensor(model, name, emel::kernel::detail::dtype_q4_k, cols, rows, q4_k_storage_bytes(cols, rows)); } @@ -743,11 +737,11 @@ TEST_CASE("moshi lm contract accounts for supported dense tensor byte widths") { CAPTURE(entry.name); auto model = std::make_unique(*loaded.model); configure_q4_k_lm_contract(*model); - set_matrix_tensor( - *model, "lm.text_emb.weight", entry.dtype, model->moshi_lm.dim, - model->moshi_lm.text_card + 1, - dense_storage_bytes(model->moshi_lm.dim, model->moshi_lm.text_card + 1, - entry.element_bytes)); + set_matrix_tensor(*model, "lm.text_emb.weight", entry.dtype, + model->moshi_lm.dim, model->moshi_lm.text_card + 1, + dense_storage_bytes(model->moshi_lm.dim, + model->moshi_lm.text_card + 1, + entry.element_bytes)); CHECK(moshi::validate_execution_contract(*model) == k_none); auto *tensor = find_mutable_tensor(*model, "lm.text_emb.weight"); @@ -769,12 +763,8 @@ TEST_CASE("moshi lm contract accepts supported gguf tensor layouts") { int32_t dtype = 0; }; const std::array cases{ - dtype_case{"f32", 0}, - dtype_case{"q4_0", 2}, - dtype_case{"q8_0", 8}, - dtype_case{"q2_k", 10}, - dtype_case{"q3_k", 11}, - dtype_case{"q4_k", 12}, + dtype_case{"f32", 0}, dtype_case{"q4_0", 2}, dtype_case{"q8_0", 8}, + dtype_case{"q2_k", 10}, dtype_case{"q3_k", 11}, dtype_case{"q4_k", 12}, dtype_case{"q6_k", 14}, }; @@ -782,19 +772,17 @@ TEST_CASE("moshi lm contract accepts supported gguf tensor layouts") { CAPTURE(entry.name); auto model = std::make_unique(*loaded.model); configure_q4_k_lm_contract(*model); - const auto layout = - emel::gguf::loader::detail::ggml_layout( - static_cast(entry.dtype)); + const auto layout = emel::gguf::loader::detail::ggml_layout( + static_cast(entry.dtype)); REQUIRE(layout.block_size > 0u); REQUIRE(layout.type_size > 0u); - REQUIRE(static_cast(model->moshi_lm.dim) % - layout.block_size == + REQUIRE(static_cast(model->moshi_lm.dim) % layout.block_size == 0u); - set_matrix_tensor( - *model, "lm.text_emb.weight", entry.dtype, model->moshi_lm.dim, - model->moshi_lm.text_card + 1, - quant_storage_bytes(model->moshi_lm.dim, model->moshi_lm.text_card + 1, - layout.block_size, layout.type_size)); + set_matrix_tensor(*model, "lm.text_emb.weight", entry.dtype, + model->moshi_lm.dim, model->moshi_lm.text_card + 1, + quant_storage_bytes(model->moshi_lm.dim, + model->moshi_lm.text_card + 1, + layout.block_size, layout.type_size)); CHECK(moshi::validate_execution_contract(*model) == k_none); auto *tensor = find_mutable_tensor(*model, "lm.text_emb.weight"); @@ -816,38 +804,36 @@ TEST_CASE("moshi lm contract rejects unsupported known gguf tensor layouts") { int32_t dtype = 0; }; const std::array cases{ - dtype_case{"f16", 1}, dtype_case{"q4_1", 3}, - dtype_case{"q5_0", 6}, - dtype_case{"q5_1", 7}, dtype_case{"q8_1", 9}, - dtype_case{"q5_k", 13}, dtype_case{"q8_k", 15}, - dtype_case{"iq2_xxs", 16}, dtype_case{"iq2_xs", 17}, - dtype_case{"iq3_xxs", 18}, dtype_case{"iq1_s", 19}, - dtype_case{"iq4_nl", 20}, dtype_case{"iq3_s", 21}, - dtype_case{"iq2_s", 22}, dtype_case{"iq4_xs", 23}, - dtype_case{"i8", 24}, dtype_case{"i16", 25}, - dtype_case{"i32", 26}, dtype_case{"i64", 27}, - dtype_case{"f64", 28}, dtype_case{"iq1_m", 29}, - dtype_case{"bf16", 30}, dtype_case{"tq1_0", 34}, - dtype_case{"tq2_0", 35}, dtype_case{"mxfp4", 39}, + dtype_case{"f16", 1}, dtype_case{"q4_1", 3}, + dtype_case{"q5_0", 6}, dtype_case{"q5_1", 7}, + dtype_case{"q8_1", 9}, dtype_case{"q5_k", 13}, + dtype_case{"q8_k", 15}, dtype_case{"iq2_xxs", 16}, + dtype_case{"iq2_xs", 17}, dtype_case{"iq3_xxs", 18}, + dtype_case{"iq1_s", 19}, dtype_case{"iq4_nl", 20}, + dtype_case{"iq3_s", 21}, dtype_case{"iq2_s", 22}, + dtype_case{"iq4_xs", 23}, dtype_case{"i8", 24}, + dtype_case{"i16", 25}, dtype_case{"i32", 26}, + dtype_case{"i64", 27}, dtype_case{"f64", 28}, + dtype_case{"iq1_m", 29}, dtype_case{"bf16", 30}, + dtype_case{"tq1_0", 34}, dtype_case{"tq2_0", 35}, + dtype_case{"mxfp4", 39}, }; for (const auto &entry : cases) { CAPTURE(entry.name); auto model = std::make_unique(*loaded.model); configure_q4_k_lm_contract(*model); - const auto layout = - emel::gguf::loader::detail::ggml_layout( - static_cast(entry.dtype)); + const auto layout = emel::gguf::loader::detail::ggml_layout( + static_cast(entry.dtype)); REQUIRE(layout.block_size > 0u); REQUIRE(layout.type_size > 0u); - REQUIRE(static_cast(model->moshi_lm.dim) % - layout.block_size == + REQUIRE(static_cast(model->moshi_lm.dim) % layout.block_size == 0u); - set_matrix_tensor( - *model, "lm.text_emb.weight", entry.dtype, model->moshi_lm.dim, - model->moshi_lm.text_card + 1, - quant_storage_bytes(model->moshi_lm.dim, model->moshi_lm.text_card + 1, - layout.block_size, layout.type_size)); + set_matrix_tensor(*model, "lm.text_emb.weight", entry.dtype, + model->moshi_lm.dim, model->moshi_lm.text_card + 1, + quant_storage_bytes(model->moshi_lm.dim, + model->moshi_lm.text_card + 1, + layout.block_size, layout.type_size)); CHECK(moshi::validate_execution_contract(*model) != k_none); } } @@ -1012,9 +998,10 @@ bool append_cloned_tensor(emel::model::data &model, return false; } -} // namespace +} // namespace -TEST_CASE("mimi contract validation rejects rvq split counts past the codec cap") { +TEST_CASE( + "mimi contract validation rejects rvq split counts past the codec cap") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1036,7 +1023,8 @@ TEST_CASE("mimi contract validation rejects rvq split counts past the codec cap" CHECK(moshi::validate_execution_contract(model) != k_none); } -TEST_CASE("mimi contract validation rejects transformer layers past the codec cap") { +TEST_CASE( + "mimi contract validation rejects transformer layers past the codec cap") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1048,17 +1036,22 @@ TEST_CASE("mimi contract validation rejects transformer layers past the codec ca // layers 2..16 for both families so every probed tensor exists. auto &model = *loaded.model; static constexpr const char *k_layer_tensors[] = { - "norm1.weight", "norm1.bias", - "norm2.weight", "norm2.bias", - "layer_scale_1.scale", "layer_scale_2.scale", - "self_attn.in_projs.0.weight", "self_attn.out_projs.0.weight", - "linear1.weight", "linear2.weight", + "norm1.weight", + "norm1.bias", + "norm2.weight", + "norm2.bias", + "layer_scale_1.scale", + "layer_scale_2.scale", + "self_attn.in_projs.0.weight", + "self_attn.out_projs.0.weight", + "linear1.weight", + "linear2.weight", }; for (const char *family : {"encoder_transformer", "decoder_transformer"}) { for (int layer = 2; layer <= 16; ++layer) { for (const char *suffix : k_layer_tensors) { - const std::string source = std::string("mimi.") + family + - ".transformer.layers.1." + suffix; + const std::string source = + std::string("mimi.") + family + ".transformer.layers.1." + suffix; const std::string clone = std::string("mimi.") + family + ".transformer.layers." + std::to_string(layer) + "." + suffix; @@ -1135,7 +1128,8 @@ TEST_CASE("mimi contract validation rejects a mixed f16 conv class") { CHECK(moshi::validate_execution_contract(model) != k_none); } -TEST_CASE("mimi contract validation requires f16 rvq projections in the f16 class") { +TEST_CASE( + "mimi contract validation requires f16 rvq projections in the f16 class") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1154,7 +1148,8 @@ TEST_CASE("mimi contract validation requires f16 rvq projections in the f16 clas const std::string_view name{model.name_storage.data() + tensor.name_offset, tensor.name_length}; const bool non_transposed_conv = - (name.starts_with("mimi.encoder.") || name.starts_with("mimi.decoder.") || + (name.starts_with("mimi.encoder.") || + name.starts_with("mimi.decoder.") || name.starts_with("mimi.downsample.")) && name.ends_with("conv.conv.weight") && name.find("convtr") == std::string_view::npos; @@ -1164,7 +1159,7 @@ TEST_CASE("mimi contract validation requires f16 rvq projections in the f16 clas tensor.type = k_dtype_f16; flipped += 1; } - REQUIRE(flipped >= 25u); // 14 encoder + 10 decoder + downsample + REQUIRE(flipped >= 25u); // 14 encoder + 10 decoder + downsample CHECK(moshi::validate_execution_contract(model) != k_none); } @@ -1196,7 +1191,8 @@ TEST_CASE("mimi contract validation rejects oversized conv geometry extents") { CHECK(moshi::validate_execution_contract(model) != k_none); } -TEST_CASE("mimi contract validation rejects a frame length off the stride chain") { +TEST_CASE( + "mimi contract validation rejects a frame length off the stride chain") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1205,11 +1201,12 @@ TEST_CASE("mimi contract validation rejects a frame length off the stride chain" // The fixed encoder/downsample stride chain reduces exactly 1920 samples // to one token; a positive finite frame rate producing any other frame // length validated here and then failed plan_seanet/plan_codec. - loaded.model->mimi.frame_rate = 25.0f; // 24000 / 25 = 960 samples + loaded.model->mimi.frame_rate = 25.0f; // 24000 / 25 = 960 samples CHECK(moshi::validate_execution_contract(*loaded.model) != k_none); } -TEST_CASE("mimi contract validation rejects codebook extents past the codec cap") { +TEST_CASE( + "mimi contract validation rejects codebook extents past the codec cap") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1237,7 +1234,8 @@ TEST_CASE("mimi contract validation rejects codebook extents past the codec cap" CHECK(moshi::validate_execution_contract(model) != k_none); } -TEST_CASE("mimi contract validation rejects non-float tensors the binder consumes") { +TEST_CASE( + "mimi contract validation rejects non-float tensors the binder consumes") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1266,17 +1264,17 @@ TEST_CASE("mimi contract validation rejects non-float tensors the binder consume }; SUBCASE("bf16 transformer vector") { - REQUIRE(flip_type( - "mimi.encoder_transformer.transformer.layers.1.norm2.bias", - k_dtype_bf16)); + REQUIRE( + flip_type("mimi.encoder_transformer.transformer.layers.1.norm2.bias", + k_dtype_bf16)); } SUBCASE("bf16 seanet conv in the f32 class") { REQUIRE(flip_type("mimi.decoder.model.0.conv.conv.weight", k_dtype_bf16)); } SUBCASE("q8_0 rvq codebook") { - REQUIRE(flip_type( - "mimi.quantizer.rvq_first.vq.layers.0._codebook.embedding", - k_dtype_q8_0)); + REQUIRE( + flip_type("mimi.quantizer.rvq_first.vq.layers.0._codebook.embedding", + k_dtype_q8_0)); } CHECK(moshi::validate_execution_contract(model) != k_none); } @@ -1298,12 +1296,12 @@ TEST_CASE("mimi contract validation rejects mixed transformer mlp widths") { tensor.name_length}; if (name == "mimi.encoder_transformer.transformer.layers.1.linear1.weight") { - tensor.dims[1] = 1; // storage stays over-sized, width shrinks + tensor.dims[1] = 1; // storage stays over-sized, width shrinks narrowed = true; } if (name == "mimi.encoder_transformer.transformer.layers.1.linear2.weight") { - tensor.dims[0] = 1; // keep linear2 consistent with the narrowed layer + tensor.dims[0] = 1; // keep linear2 consistent with the narrowed layer } } REQUIRE(narrowed); @@ -1380,7 +1378,8 @@ TEST_CASE("mimi contract validation requires the fixed seanet topology") { CHECK(moshi::validate_execution_contract(model) != k_none); } -TEST_CASE("mimi contract validation rejects q8 classes with misaligned widths") { +TEST_CASE( + "mimi contract validation rejects q8 classes with misaligned widths") { auto loaded = load_fixture_or_skip("mimi-tiny.gguf"); if (loaded.model == nullptr) { return; @@ -1402,7 +1401,7 @@ TEST_CASE("mimi contract validation rejects q8 classes with misaligned widths") name.find("linear2.weight") != std::string_view::npos || name.find("_proj.weight") != std::string_view::npos; if (projection && name.starts_with("mimi.")) { - tensor.type = 8; // q8_0 + tensor.type = 8; // q8_0 flipped += 1; } } @@ -1470,9 +1469,7 @@ TEST_CASE("mimi contract validation requires the rvq projections") { const size_t underscore = name.find("input_proj") + 5u; model.name_storage[tensor.name_offset + underscore] = 'x'; } - SUBCASE("wrong projection element count") { - tensor.dims[0] += 1; - } + SUBCASE("wrong projection element count") { tensor.dims[0] += 1; } corrupted = true; } REQUIRE(corrupted); @@ -1519,9 +1516,7 @@ TEST_CASE("mimi contract validation requires every acoustic rvq codebook") { const size_t digit = name.find("layers.1") + 7u; model.name_storage[tensor.name_offset + digit] = '9'; } - SUBCASE("wrong intermediate codebook shape") { - tensor.dims[1] += 1; - } + SUBCASE("wrong intermediate codebook shape") { tensor.dims[1] += 1; } corrupted = true; } REQUIRE(corrupted); diff --git a/tests/speech/codec/mimi_lifecycle_tests.cpp b/tests/speech/codec/mimi_lifecycle_tests.cpp index 424be2b0..e730df29 100644 --- a/tests/speech/codec/mimi_lifecycle_tests.cpp +++ b/tests/speech/codec/mimi_lifecycle_tests.cpp @@ -1,7 +1,7 @@ #include "doctest/doctest.h" -#include #include +#include #include #include #include @@ -410,9 +410,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { // 256-float buffer while the declared extents describe 2^64 elements. for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; if (name == "mimi.encoder_transformer.transformer.layers.0.self_attn." "out_projs.0.weight") { tensor.n_dims = 2; @@ -428,9 +428,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { // time with no error channel, so the plan must reject the q8 class. for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; const bool projection = name.find("self_attn.in_projs.0.weight") != std::string_view::npos || name.find("self_attn.out_projs.0.weight") != std::string_view::npos || @@ -438,7 +438,7 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { name.find("linear2.weight") != std::string_view::npos || name.find("_proj.weight") != std::string_view::npos; if (projection && name.starts_with("mimi.")) { - tensor.type = 8; // q8_0 + tensor.type = 8; // q8_0 } } expect_bind_failed(*loaded.model); @@ -457,9 +457,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { // one-tap upsample kernel against stride 2 would index outside it. for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; if (name == "mimi.upsample.convtr.convtr.convtr.weight") { tensor.dims[0] = 1; } @@ -470,9 +470,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { SUBCASE("seanet transposed layer kernel shorter than its stride") { for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; if (name.find("decoder.model.") != std::string_view::npos && name.find(".convtr.convtr.weight") != std::string_view::npos) { tensor.dims[0] = 1; @@ -504,9 +504,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { // span negative in the sizing accounting. for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; if (name.find("encoder.model.") != std::string_view::npos && name.find(".conv.conv.weight") != std::string_view::npos && name.find(".block.") == std::string_view::npos) { @@ -519,9 +519,9 @@ TEST_CASE("mimi codec initialize rejects out-of-contract model metadata") { SUBCASE("downsample conv kernel shorter than its stride") { for (uint32_t index = 0u; index < loaded.model->n_tensors; ++index) { auto &tensor = loaded.model->tensors[index]; - const std::string_view name{ - loaded.model->name_storage.data() + tensor.name_offset, - tensor.name_length}; + const std::string_view name{loaded.model->name_storage.data() + + tensor.name_offset, + tensor.name_length}; if (name == "mimi.downsample.conv.conv.conv.weight") { tensor.dims[0] = 1; } @@ -667,8 +667,9 @@ TEST_CASE("mimi codec facade rejects malformed frame requests explicitly") { std::span{codes}}; bad_encode.error_out = &err; g_encode_error_count = 0; - bad_encode.on_error = emel::callback::from<&on_encode_frame_error>(); + bad_encode.on_error = + emel::callback::from< + &on_encode_frame_error>(); CHECK_FALSE(machine.process_event(bad_encode)); CHECK(g_encode_error_count == 1u); CHECK(err == emel::error::cast(mimi::error::request_shape)); @@ -681,8 +682,9 @@ TEST_CASE("mimi codec facade rejects malformed frame requests explicitly") { std::span{short_pcm_out}}; bad_decode.error_out = &err; g_decode_error_count = 0; - bad_decode.on_error = emel::callback::from<&on_decode_frame_error>(); + bad_decode.on_error = + emel::callback::from< + &on_decode_frame_error>(); CHECK_FALSE(machine.process_event(bad_decode)); CHECK(g_decode_error_count == 1u); CHECK(err == emel::error::cast(mimi::error::request_shape)); @@ -751,11 +753,10 @@ TEST_CASE("mimi codec facade streams the f16 and q8 operand-class fixtures") { emel::error::type err = emel::error::cast(mimi::error::none); g_frame_samples = 0; g_n_q = 0; - mimi::event::initialize init{*loaded.model, - std::span{arenas.prepared}, - std::span{arenas.state}, - std::span{arenas.workspace}, - std::span{arenas.frame}}; + mimi::event::initialize init{ + *loaded.model, std::span{arenas.prepared}, + std::span{arenas.state}, std::span{arenas.workspace}, + std::span{arenas.frame}}; init.error_out = &err; init.on_done = emel::callback::from<&on_initialize_done>(); @@ -851,8 +852,7 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { { quantizer::sm machine{}; emel::error::type err = emel::error::cast(quantizer::error::none); - quantizer::event::encode ev{unbound_runtime, - std::span{tiny_buf}, + quantizer::event::encode ev{unbound_runtime, std::span{tiny_buf}, std::span{tiny_codes}, std::span{tiny_buf}}; ev.error_out = &err; @@ -860,10 +860,9 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { CHECK(err == emel::error::cast(quantizer::error::runtime_unbound)); err = emel::error::cast(quantizer::error::none); - quantizer::event::decode dev{unbound_runtime, - std::span{tiny_codes}, - std::span{tiny_buf}, - std::span{tiny_buf}}; + quantizer::event::decode dev{ + unbound_runtime, std::span{tiny_codes}, + std::span{tiny_buf}, std::span{tiny_buf}}; dev.error_out = &err; CHECK_FALSE(machine.process_event(dev)); CHECK(err == emel::error::cast(quantizer::error::runtime_unbound)); @@ -893,12 +892,13 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { // pcm shorter than one frame -> request_shape encoder::sm machine{}; emel::error::type err = emel::error::cast(encoder::error::none); - encoder::event::encode ev{runtime, - streaming, - std::span{pcm.data(), pcm.size() - 1u}, - std::span{frame}, - std::span{workspace}, - std::span{latent}}; + encoder::event::encode ev{ + runtime, + streaming, + std::span{pcm.data(), pcm.size() - 1u}, + std::span{frame}, + std::span{workspace}, + std::span{latent}}; ev.error_out = &err; CHECK_FALSE(machine.process_event(ev)); CHECK(err == emel::error::cast(encoder::error::request_shape)); @@ -933,13 +933,13 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { // latent narrower than dim -> request_shape decoder::sm machine{}; emel::error::type err = emel::error::cast(decoder::error::none); - decoder::event::decode ev{runtime, - streaming, - std::span{latent.data(), - latent.size() - 1u}, - std::span{frame}, - std::span{workspace}, - std::span{pcm}}; + decoder::event::decode ev{ + runtime, + streaming, + std::span{latent.data(), latent.size() - 1u}, + std::span{frame}, + std::span{workspace}, + std::span{pcm}}; ev.error_out = &err; CHECK_FALSE(machine.process_event(ev)); CHECK(err == emel::error::cast(decoder::error::request_shape)); @@ -974,11 +974,9 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { // code_range quantizer::sm machine{}; emel::error::type err = emel::error::cast(quantizer::error::none); - quantizer::event::encode ev{runtime, - std::span{latent.data(), - latent.size() - 1u}, - std::span{codes}, - std::span{workspace}}; + quantizer::event::encode ev{ + runtime, std::span{latent.data(), latent.size() - 1u}, + std::span{codes}, std::span{workspace}}; ev.error_out = &err; CHECK_FALSE(machine.process_event(ev)); CHECK(err == emel::error::cast(quantizer::error::request_shape)); @@ -986,8 +984,7 @@ TEST_CASE("mimi child actors report validation errors when driven directly") { err = emel::error::cast(quantizer::error::none); std::vector bad_codes(codes.size(), 0); bad_codes[0] = runtime.quantizer.codebook_entries; - quantizer::event::decode dev{runtime, - std::span{bad_codes}, + quantizer::event::decode dev{runtime, std::span{bad_codes}, std::span{latent}, std::span{workspace}}; dev.error_out = &err; diff --git a/tests/speech/decoder/whisper/lifecycle_tests.cpp b/tests/speech/decoder/whisper/lifecycle_tests.cpp index d4c6e371..ec6ceb73 100644 --- a/tests/speech/decoder/whisper/lifecycle_tests.cpp +++ b/tests/speech/decoder/whisper/lifecycle_tests.cpp @@ -17,15 +17,14 @@ #include "emel/gguf/loader/detail.hpp" #include "emel/gguf/loader/events.hpp" #include "emel/gguf/loader/sm.hpp" -#include "emel/speech/encoder/whisper/detail.hpp" #include "emel/model/detail.hpp" #include "emel/model/loader/errors.hpp" -#include "emel/speech/decoder/whisper/any.hpp" #include "emel/model/whisper/detail.hpp" +#include "emel/speech/decoder/whisper/any.hpp" #include "emel/speech/decoder/whisper/detail.hpp" #include "emel/speech/decoder/whisper/sm.hpp" +#include "emel/speech/encoder/whisper/detail.hpp" #include "emel/speech/encoder/whisper/sm.hpp" -#include "emel/speech/recognizer_routes/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/any.hpp" namespace { @@ -51,16 +50,16 @@ struct decoder_error_capture { emel::error::type err = emel::error::cast(decoder::error::none); }; -void record_decoder_done(decoder_done_capture & capture, - const decoder::events::decode_done & done) noexcept { +void record_decoder_done(decoder_done_capture &capture, + const decoder::events::decode_done &done) noexcept { ++capture.calls; capture.token = done.token; capture.confidence = done.confidence; capture.digest = done.digest; } -void record_decoder_error(decoder_error_capture & capture, - const decoder::events::decode_error & error) noexcept { +void record_decoder_error(decoder_error_capture &capture, + const decoder::events::decode_error &error) noexcept { ++capture.calls; capture.err = error.err; } @@ -77,7 +76,7 @@ std::filesystem::path whisper_fixture_path() { return repo_root() / "tests" / "models" / "model-tiny-q80.gguf"; } -std::vector read_binary_file(const std::filesystem::path & path) { +std::vector read_binary_file(const std::filesystem::path &path) { std::ifstream stream(path, std::ios::binary); REQUIRE(stream.good()); stream.seekg(0, std::ios::end); @@ -90,34 +89,34 @@ std::vector read_binary_file(const std::filesystem::path & path) { return bytes; } -std::string read_text_file(const std::filesystem::path & path) { +std::string read_text_file(const std::filesystem::path &path) { std::ifstream stream(path); REQUIRE(stream.good()); return std::string{std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; } -void materialize_tensor_names_from_file(emel::model::data & model, - const std::vector & file_bytes) { +void materialize_tensor_names_from_file( + emel::model::data &model, const std::vector &file_bytes) { model.name_bytes_used = 0u; for (uint32_t index = 0u; index < model.n_tensors; ++index) { - auto & tensor = model.tensors[index]; + auto &tensor = model.tensors[index]; const size_t source_offset = static_cast(tensor.name_offset); const size_t length = static_cast(tensor.name_length); REQUIRE(source_offset + length <= file_bytes.size()); - REQUIRE(static_cast(model.name_bytes_used) + length <= model.name_storage.size()); + REQUIRE(static_cast(model.name_bytes_used) + length <= + model.name_storage.size()); std::memcpy(model.name_storage.data() + model.name_bytes_used, - file_bytes.data() + source_offset, - length); + file_bytes.data() + source_offset, length); tensor.name_offset = model.name_bytes_used; model.name_bytes_used += static_cast(length); } } -bool load_whisper_fixture_binding(const std::vector & file_bytes, - std::vector & kv_arena, - std::vector & kv_entries, - emel::model::data & model_out) { +bool load_whisper_fixture_binding( + const std::vector &file_bytes, std::vector &kv_arena, + std::vector &kv_entries, + emel::model::data &model_out) { emel::gguf::loader::sm loader{}; emel::gguf::loader::requirements requirements = {}; const auto on_probe_done = @@ -134,7 +133,8 @@ bool load_whisper_fixture_binding(const std::vector & file_bytes, emel::gguf::loader::event::parse_error_fn::from<&noop_parse_error>(); const emel::gguf::loader::event::probe probe{ - std::span{file_bytes}, requirements, on_probe_done, on_probe_error}; + std::span{file_bytes}, requirements, on_probe_done, + on_probe_error}; if (!loader.process_event(probe) || requirements.tensor_count == 0u || requirements.tensor_count > model_out.tensors.size()) { return false; @@ -150,8 +150,7 @@ bool load_whisper_fixture_binding(const std::vector & file_bytes, std::span{kv_entries}, std::span{model_out.tensors.data(), model_out.n_tensors}, - on_bind_done, - on_bind_error}; + on_bind_done, on_bind_error}; if (!loader.process_event(bind)) { return false; } @@ -166,25 +165,27 @@ struct loaded_whisper_fixture { std::vector kv_arena = {}; std::vector kv_entries = {}; std::unique_ptr model = {}; - emel::speech::encoder::whisper::detail::execution_contract encoder_contract = {}; + emel::speech::encoder::whisper::detail::execution_contract encoder_contract = + {}; decoder::detail::execution_contract decoder_contract = {}; }; -void mark_decoder_aux_tensors_f32(emel::model::data & model) { +void mark_decoder_aux_tensors_f32(emel::model::data &model) { for (uint32_t index = 0; index < model.n_tensors; ++index) { - auto & tensor = model.tensors[index]; + auto &tensor = model.tensors[index]; const auto name = emel::model::tensor_name_view(model, tensor); if (name.starts_with("model.decoder.") && - (tensor.n_dims == 1 || name == "model.decoder.embed_positions.weight")) { + (tensor.n_dims == 1 || + name == "model.decoder.embed_positions.weight")) { tensor.type = static_cast(emel::kernel::detail::dtype_f32); } } } -void mark_decoder_linear_weights(emel::model::data & model, +void mark_decoder_linear_weights(emel::model::data &model, const int32_t dtype) { for (uint32_t index = 0; index < model.n_tensors; ++index) { - auto & tensor = model.tensors[index]; + auto &tensor = model.tensors[index]; const auto name = emel::model::tensor_name_view(model, tensor); if (name.starts_with("model.decoder.layers.") && tensor.n_dims == 2 && name.ends_with(".weight")) { @@ -197,17 +198,19 @@ void mark_decoder_linear_weights(emel::model::data & model, // shared zero-filled f32 arena so a q8_0+f32-aux decode can run end to end // with in-bounds aux reads. The caller must keep the returned arena alive // for the lifetime of the mutated model. -std::vector repoint_decoder_aux_tensors_to_f32(emel::model::data & model) { +std::vector +repoint_decoder_aux_tensors_to_f32(emel::model::data &model) { namespace whisper = emel::speech::decoder::whisper::detail; std::vector arena( static_cast(whisper::k_embedding_length) * static_cast(whisper::k_decoder_sequence_token_count), 0.0f); for (uint32_t index = 0; index < model.n_tensors; ++index) { - auto & tensor = model.tensors[index]; + auto &tensor = model.tensors[index]; const auto name = emel::model::tensor_name_view(model, tensor); if (name.starts_with("model.decoder.") && - (tensor.n_dims == 1 || name == "model.decoder.embed_positions.weight")) { + (tensor.n_dims == 1 || + name == "model.decoder.embed_positions.weight")) { uint64_t count = 1u; for (int32_t dim = 0; dim < tensor.n_dims; ++dim) { count *= static_cast(tensor.dims[static_cast(dim)]); @@ -232,25 +235,26 @@ loaded_whisper_fixture load_fixture_or_skip() { loaded_whisper_fixture loaded{}; loaded.model = std::make_unique(); loaded.file_bytes = read_binary_file(fixture_path); - REQUIRE(load_whisper_fixture_binding(loaded.file_bytes, - loaded.kv_arena, - loaded.kv_entries, - *loaded.model)); + REQUIRE(load_whisper_fixture_binding(loaded.file_bytes, loaded.kv_arena, + loaded.kv_entries, *loaded.model)); const emel::model::detail::kv_binding binding{ .arena = std::span{loaded.kv_arena}, - .entries = std::span{loaded.kv_entries}, + .entries = + std::span{loaded.kv_entries}, }; REQUIRE(emel::model::detail::load_hparams_from_gguf(binding, *loaded.model)); loaded.model->weights_data = loaded.file_bytes.data(); loaded.model->weights_size = loaded.file_bytes.size(); materialize_tensor_names_from_file(*loaded.model, loaded.file_bytes); emel::model::whisper::detail::execution_contract model_contract = {}; - REQUIRE(emel::model::whisper::detail::build_execution_contract(*loaded.model, - model_contract) == + REQUIRE(emel::model::whisper::detail::build_execution_contract( + *loaded.model, model_contract) == emel::error::cast(emel::model::loader::error::none)); loaded.encoder_contract = - emel::speech::encoder::whisper::detail::bind_execution_contract(*loaded.model); - loaded.decoder_contract = decoder::detail::bind_execution_contract(*loaded.model); + emel::speech::encoder::whisper::detail::bind_execution_contract( + *loaded.model); + loaded.decoder_contract = + decoder::detail::bind_execution_contract(*loaded.model); return loaded; } @@ -271,26 +275,30 @@ struct encoded_fixture { uint64_t digest = 0; }; -encoded_fixture encode_fixture_audio(const loaded_whisper_fixture & loaded) { +encoded_fixture encode_fixture_audio(const loaded_whisper_fixture &loaded) { const std::vector pcm = deterministic_pcm(320); encoded_fixture encoded{}; encoded.encoder_workspace.resize(static_cast( - emel::speech::encoder::whisper::detail::required_workspace_floats(pcm.size()))); + emel::speech::encoder::whisper::detail::required_workspace_floats( + pcm.size()))); encoded.encoder_state.resize(static_cast( - emel::speech::encoder::whisper::detail::required_encoder_output_floats(pcm.size()))); + emel::speech::encoder::whisper::detail::required_encoder_output_floats( + pcm.size()))); emel::speech::encoder::whisper::sm encoder{}; - emel::speech::encoder::whisper::event::encode request{loaded.encoder_contract, - pcm, - 16000, - 1, - encoded.encoder_workspace, - encoded.encoder_state, - encoded.frames, - encoded.width, - encoded.digest}; + emel::speech::encoder::whisper::event::encode request{ + loaded.encoder_contract, + pcm, + 16000, + 1, + encoded.encoder_workspace, + encoded.encoder_state, + encoded.frames, + encoded.width, + encoded.digest}; REQUIRE(encoder.process_event(request)); REQUIRE(encoded.frames > 0); - REQUIRE(encoded.width == emel::speech::encoder::whisper::detail::k_embedding_length); + REQUIRE(encoded.width == + emel::speech::encoder::whisper::detail::k_embedding_length); return encoded; } @@ -359,8 +367,8 @@ zero_q4_decoder_fixture make_zero_q4_decoder_fixture() { add_q4_weight("model.decoder.embed_tokens.weight", whisper::k_embedding_length, whisper::k_vocab_size); add_q8_aux("model.decoder.embed_positions.weight", 2, - {whisper::k_embedding_length, whisper::k_decoder_sequence_token_count, - 0, 0}); + {whisper::k_embedding_length, + whisper::k_decoder_sequence_token_count, 0, 0}); add_q8_aux("model.decoder.layer_norm.weight", 1, {whisper::k_embedding_length, 0, 0, 0}); add_q8_aux("model.decoder.layer_norm.bias", 1, @@ -409,15 +417,13 @@ zero_q4_decoder_fixture make_zero_q4_decoder_fixture() { {whisper::k_feed_forward_length, 0, 0, 0}); add_q4_weight(prefix + "fc2.weight", whisper::k_feed_forward_length, whisper::k_embedding_length); - add_q8_aux(prefix + "fc2.bias", 1, - {whisper::k_embedding_length, 0, 0, 0}); + add_q8_aux(prefix + "fc2.bias", 1, {whisper::k_embedding_length, 0, 0, 0}); } return fixture; } -template -void exercise_q4_decoder_linear_shape() { +template void exercise_q4_decoder_linear_shape() { namespace quant = emel::kernel::detail::quant; namespace whisper = emel::speech::decoder::whisper::detail; @@ -439,16 +445,16 @@ void exercise_q4_decoder_linear_shape() { emel::model::data::tensor_record q4_0_tensor{}; q4_0_tensor.type = static_cast(emel::kernel::detail::dtype_q4_0); q4_0_tensor.n_dims = 2; - q4_0_tensor.dims = { - static_cast(In), static_cast(Out), 0, 0}; + q4_0_tensor.dims = {static_cast(In), static_cast(Out), 0, + 0}; q4_0_tensor.data = q4_0_blocks.data(); q4_0_tensor.data_size = q4_0_blocks.size() * sizeof(quant::block_q4_0); emel::model::data::tensor_record q4_1_tensor{}; q4_1_tensor.type = static_cast(emel::kernel::detail::dtype_q4_1); q4_1_tensor.n_dims = 2; - q4_1_tensor.dims = { - static_cast(In), static_cast(Out), 0, 0}; + q4_1_tensor.dims = {static_cast(In), static_cast(Out), 0, + 0}; q4_1_tensor.data = q4_1_blocks.data(); q4_1_tensor.data_size = q4_1_blocks.size() * sizeof(quant::block_q4_1); @@ -491,7 +497,7 @@ void exercise_q4_decoder_linear_shape() { } } -} // namespace +} // namespace TEST_CASE("whisper_decoder_runtime_owns_decode_detail_dependencies") { const auto root = repo_root(); @@ -503,13 +509,15 @@ TEST_CASE("whisper_decoder_runtime_owns_decode_detail_dependencies") { for (const auto &path : production_files) { const std::string source = read_text_file(path); - CHECK(source.find("emel/speech/encoder/whisper/detail.hpp") == std::string::npos); + CHECK(source.find("emel/speech/encoder/whisper/detail.hpp") == + std::string::npos); CHECK(source.find("encoder::whisper::detail") == std::string::npos); } const std::string decoder_detail = read_text_file(production_files[2]); CHECK(decoder_detail.find("run_decoder_sequence") != std::string::npos); - CHECK(decoder_detail.find("select_greedy_timestamp_aware_token") != std::string::npos); + CHECK(decoder_detail.find("select_greedy_timestamp_aware_token") != + std::string::npos); } TEST_CASE("whisper_decoder_detail_timestamp_blocking_is_decoder_owned") { @@ -519,8 +527,7 @@ TEST_CASE("whisper_decoder_detail_timestamp_blocking_is_decoder_owned") { -1000.0f); logits[42] = 100.0f; logits[static_cast(whisper::k_token_timestamp_begin)] = 0.0f; - const std::array generated{42, - whisper::k_token_timestamp_begin}; + const std::array generated{42, whisper::k_token_timestamp_begin}; const whisper::decode_policy_runtime policy{}; float confidence = 0.0f; const int32_t token = whisper::select_greedy_timestamp_aware_token( @@ -534,8 +541,8 @@ TEST_CASE("whisper_decoder_detail_timestamp_policy_suppresses_control_tokens") { const whisper::decode_policy_runtime policy{}; - std::vector initial_logits( - static_cast(whisper::k_vocab_size), -1000.0f); + std::vector initial_logits(static_cast(whisper::k_vocab_size), + -1000.0f); initial_logits[static_cast(policy.eot)] = 500.0f; initial_logits[static_cast(policy.space)] = 400.0f; initial_logits[42] = 10.0f; @@ -545,8 +552,8 @@ TEST_CASE("whisper_decoder_detail_timestamp_policy_suppresses_control_tokens") { CHECK(initial_token == 42); CHECK(initial_confidence == doctest::Approx(10.0f)); - std::vector control_logits( - static_cast(whisper::k_vocab_size), -1000.0f); + std::vector control_logits(static_cast(whisper::k_vocab_size), + -1000.0f); control_logits[static_cast(policy.sot)] = 600.0f; control_logits[static_cast(policy.translate)] = 500.0f; control_logits[static_cast(policy.transcribe)] = 400.0f; @@ -570,13 +577,10 @@ TEST_CASE("whisper_decoder_generation_budget_matches_text_context") { } TEST_CASE("whisper_decoder_public_generation_budget_hooks_match_detail") { - namespace route = emel::speech::recognizer_routes::whisper; namespace whisper = emel::speech::decoder::whisper::detail; CHECK(decoder::max_generated_token_count() == whisper::k_max_generated_token_count); - CHECK(route::max_generated_token_count() == - whisper::k_max_generated_token_count); } TEST_CASE("whisper_decoder_detail_reads_f32_aux_and_q4_linear_rows") { @@ -636,9 +640,9 @@ TEST_CASE("whisper_decoder_detail_reads_f32_aux_and_q4_linear_rows") { q4_0_tensor, 0u, input.data(), input.size()) == doctest::Approx(8.0f)); emel::kernel::sm linear_kernel{emel::kernel::detect_host_kind()}; - whisper::linear_no_bias( - linear_kernel, q4_0_tensor, input.data(), output.data()); + whisper::linear_no_bias(linear_kernel, q4_0_tensor, input.data(), + output.data()); CHECK(output[0] == doctest::Approx(8.0f)); CHECK(whisper::read_matrix_q4_1_value(q4_1_tensor, 0u, 0u) == @@ -656,7 +660,8 @@ TEST_CASE("whisper_decoder_detail_reads_f32_aux_and_q4_linear_rows") { std::array(whisper::k_embedding_length)> norm_in{}; std::array(whisper::k_embedding_length)> norm_w{}; std::array(whisper::k_embedding_length)> norm_b{}; - std::array(whisper::k_embedding_length)> norm_out{}; + std::array(whisper::k_embedding_length)> + norm_out{}; for (size_t index = 0; index < norm_in.size(); ++index) { norm_in[index] = static_cast(index % 7u); norm_w[index] = 1.0f; @@ -765,8 +770,8 @@ TEST_CASE("whisper_decoder_rejects_invalid_runtime_capacity") { } auto encoded = encode_fixture_audio(loaded); - std::vector workspace(static_cast( - decoder::detail::required_decoder_workspace_floats( + std::vector workspace( + static_cast(decoder::detail::required_decoder_workspace_floats( static_cast(encoded.frames)))); std::vector logits(static_cast(decoder::detail::k_vocab_size)); const auto &policy = @@ -778,40 +783,45 @@ TEST_CASE("whisper_decoder_rejects_invalid_runtime_capacity") { int32_t token = 0; float confidence = 0.0f; uint64_t digest = 0; - emel::error::type err = emel::error::cast(emel::speech::decoder::whisper::error::none); + emel::error::type err = + emel::error::cast(emel::speech::decoder::whisper::error::none); emel::speech::decoder::whisper::sm decoder{}; - emel::speech::decoder::whisper::event::decode bad_policy{loaded.decoder_contract, - encoded.encoder_state, - encoded.frames, - unsupported_policy, - generated_tokens, - generated_token_count, - workspace, - logits, - token, - confidence, - digest}; + emel::speech::decoder::whisper::event::decode bad_policy{ + loaded.decoder_contract, + encoded.encoder_state, + encoded.frames, + unsupported_policy, + generated_tokens, + generated_token_count, + workspace, + logits, + token, + confidence, + digest}; bad_policy.error_out = &err; CHECK_FALSE(decoder.process_event(bad_policy)); - CHECK(err == emel::error::cast(emel::speech::decoder::whisper::error::decode_policy)); + CHECK(err == emel::error::cast( + emel::speech::decoder::whisper::error::decode_policy)); std::vector small_logits(16u); err = emel::error::cast(emel::speech::decoder::whisper::error::none); - emel::speech::decoder::whisper::event::decode bad_logits{loaded.decoder_contract, - encoded.encoder_state, - encoded.frames, - policy, - generated_tokens, - generated_token_count, - workspace, - small_logits, - token, - confidence, - digest}; + emel::speech::decoder::whisper::event::decode bad_logits{ + loaded.decoder_contract, + encoded.encoder_state, + encoded.frames, + policy, + generated_tokens, + generated_token_count, + workspace, + small_logits, + token, + confidence, + digest}; bad_logits.error_out = &err; CHECK_FALSE(decoder.process_event(bad_logits)); - CHECK(err == emel::error::cast(emel::speech::decoder::whisper::error::logits_capacity)); + CHECK(err == emel::error::cast( + emel::speech::decoder::whisper::error::logits_capacity)); std::vector empty_generated_tokens; err = emel::error::cast(emel::speech::decoder::whisper::error::none); @@ -829,8 +839,9 @@ TEST_CASE("whisper_decoder_rejects_invalid_runtime_capacity") { digest}; bad_generated_capacity.error_out = &err; CHECK_FALSE(decoder.process_event(bad_generated_capacity)); - CHECK(err == emel::error::cast( - emel::speech::decoder::whisper::error::generated_token_capacity)); + CHECK(err == + emel::error::cast( + emel::speech::decoder::whisper::error::generated_token_capacity)); } TEST_CASE("whisper_decoder_rejects_invalid_state_storage_and_model_contracts") { @@ -840,8 +851,8 @@ TEST_CASE("whisper_decoder_rejects_invalid_state_storage_and_model_contracts") { } auto encoded = encode_fixture_audio(loaded); - std::vector workspace(static_cast( - decoder::detail::required_decoder_workspace_floats( + std::vector workspace( + static_cast(decoder::detail::required_decoder_workspace_floats( static_cast(encoded.frames)))); std::vector logits(static_cast(decoder::detail::k_vocab_size)); const auto &policy = @@ -853,7 +864,8 @@ TEST_CASE("whisper_decoder_rejects_invalid_state_storage_and_model_contracts") { uint64_t digest = 0; emel::error::type err = emel::error::cast(decoder::error::none); - decoder::detail::execution_contract invalid_contract = loaded.decoder_contract; + decoder::detail::execution_contract invalid_contract = + loaded.decoder_contract; invalid_contract.model = nullptr; decoder_error_capture error_capture{}; decoder::sm invalid_model_machine{}; @@ -910,7 +922,7 @@ TEST_CASE("whisper_decoder_rejects_invalid_state_storage_and_model_contracts") { CHECK_FALSE(workspace_machine.process_event(bad_workspace)); CHECK(err == emel::error::cast(decoder::error::workspace_capacity)); - auto * variant_tensor = const_cast( + auto *variant_tensor = const_cast( emel::speech::encoder::whisper::detail::find_tensor( *loaded.model, "model.decoder.layers.0.self_attn.k_proj.weight")); REQUIRE(variant_tensor != nullptr); @@ -941,8 +953,8 @@ TEST_CASE("whisper_decoder_runs_first_q8_token_from_public_event") { } auto encoded = encode_fixture_audio(loaded); - std::vector workspace(static_cast( - decoder::detail::required_decoder_workspace_floats( + std::vector workspace( + static_cast(decoder::detail::required_decoder_workspace_floats( static_cast(encoded.frames)))); std::vector logits(static_cast(decoder::detail::k_vocab_size)); const auto &policy = @@ -952,20 +964,21 @@ TEST_CASE("whisper_decoder_runs_first_q8_token_from_public_event") { int32_t token = 0; float confidence = 0.0f; uint64_t digest = 0; - emel::error::type err = emel::error::cast(emel::speech::decoder::whisper::error::none); + emel::error::type err = + emel::error::cast(emel::speech::decoder::whisper::error::none); emel::speech::decoder::whisper::sm decoder{}; emel::speech::decoder::whisper::event::decode request{loaded.decoder_contract, - encoded.encoder_state, - encoded.frames, - policy, - generated_tokens, - generated_token_count, - workspace, - logits, - token, - confidence, - digest}; + encoded.encoder_state, + encoded.frames, + policy, + generated_tokens, + generated_token_count, + workspace, + logits, + token, + confidence, + digest}; request.error_out = &err; CHECK(decoder.process_event(request)); CHECK(err == emel::error::cast(emel::speech::decoder::whisper::error::none)); @@ -1015,17 +1028,18 @@ TEST_CASE("whisper_decoder_runs_first_q8_token_from_public_event") { int32_t token_again = 0; float confidence_again = 0.0f; uint64_t digest_again = 0; - emel::speech::decoder::whisper::event::decode repeat{loaded.decoder_contract, - encoded.encoder_state, - encoded.frames, - policy, - generated_tokens_again, - generated_token_count_again, - workspace_again, - logits_again, - token_again, - confidence_again, - digest_again}; + emel::speech::decoder::whisper::event::decode repeat{ + loaded.decoder_contract, + encoded.encoder_state, + encoded.frames, + policy, + generated_tokens_again, + generated_token_count_again, + workspace_again, + logits_again, + token_again, + confidence_again, + digest_again}; CHECK(decoder.process_event(repeat)); CHECK(token_again == token); CHECK(confidence_again == doctest::Approx(confidence)); @@ -1050,16 +1064,14 @@ TEST_CASE("whisper_decoder_runs_first_q8_token_from_public_event") { stop_policy.timestamp_begin = 0; stop_policy.space = -7; emel::kernel::sm stop_kernel{emel::kernel::detect_host_kind()}; - const uint64_t stop_digest = - decoder::detail::run_decoder_sequence< - decoder::detail::linear_weight_variant::q8_0>( - stop_kernel, - *loaded.decoder_contract.model, encoded.encoder_state.data(), - static_cast(encoded.frames), stop_policy, - policy.prompt_tokens.data(), policy.prompt_tokens.size(), - workspace.data(), logits.data(), stop_generated_tokens.data(), - stop_generated_tokens.size(), stop_generated_token_count, stop_token, - stop_confidence); + const uint64_t stop_digest = decoder::detail::run_decoder_sequence< + decoder::detail::linear_weight_variant::q8_0>( + stop_kernel, *loaded.decoder_contract.model, encoded.encoder_state.data(), + static_cast(encoded.frames), stop_policy, + policy.prompt_tokens.data(), policy.prompt_tokens.size(), + workspace.data(), logits.data(), stop_generated_tokens.data(), + stop_generated_tokens.size(), stop_generated_token_count, stop_token, + stop_confidence); CHECK(stop_generated_token_count == 2u); CHECK(stop_generated_tokens[1] >= stop_policy.timestamp_begin); CHECK(stop_digest != 0u); @@ -1210,8 +1222,8 @@ TEST_CASE("whisper_decoder_routes_q8_linear_f32_aux_variant") { } mark_decoder_aux_tensors_f32(*loaded.model); - std::vector encoder_state(static_cast( - decoder::detail::k_embedding_length)); + std::vector encoder_state( + static_cast(decoder::detail::k_embedding_length)); std::vector workspace; std::vector logits; const auto &policy = diff --git a/tests/speech/encoder/whisper/lifecycle_tests.cpp b/tests/speech/encoder/whisper/lifecycle_tests.cpp index bef8811a..c88a684e 100644 --- a/tests/speech/encoder/whisper/lifecycle_tests.cpp +++ b/tests/speech/encoder/whisper/lifecycle_tests.cpp @@ -19,11 +19,12 @@ #include "emel/model/detail.hpp" #include "emel/model/loader/errors.hpp" #include "emel/model/whisper/detail.hpp" +#include "emel/speech/decoder/whisper/any.hpp" +#include "emel/speech/encoder/whisper/any.hpp" #include "emel/speech/encoder/whisper/detail.hpp" #include "emel/speech/encoder/whisper/sm.hpp" -#include "emel/speech/recognizer/sm.hpp" -#include "emel/speech/recognizer_routes/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/any.hpp" +#include "emel/speech/transcriber/sm.hpp" namespace { @@ -536,7 +537,7 @@ TEST_CASE("whisper_encoder_runs_q8_f32_aux_variant_from_public_event") { CHECK(machine.q4_1_dispatch_count() == 0u); } -TEST_CASE("whisper_recognizer_runs_fixture_through_public_actor") { +TEST_CASE("whisper_transcriber_runs_fixture_through_public_actor") { auto loaded = load_fixture_or_skip(); if (loaded.model == nullptr) { return; @@ -544,16 +545,20 @@ TEST_CASE("whisper_recognizer_runs_fixture_through_public_actor") { const std::string tokenizer_json = read_text_file(whisper_tokenizer_path()); const std::vector pcm = deterministic_pcm(320); - namespace recognizer = emel::speech::recognizer; - namespace route = emel::speech::recognizer_routes::whisper; + namespace transcriber = emel::speech::transcriber; + namespace whisper_decoder = emel::speech::decoder::whisper; + namespace whisper_encoder = emel::speech::encoder::whisper; std::vector encoder_workspace(static_cast( - route::required_encoder_workspace_floats(pcm.size()))); - std::vector encoder_state( - static_cast(route::required_encoder_state_floats(pcm.size()))); + whisper_encoder::required_workspace_floats(pcm.size()))); + std::vector encoder_state(static_cast( + whisper_encoder::required_encoder_output_floats(pcm.size()))); + const uint64_t decoder_input_frames = + whisper_encoder::encoder_frame_count_for_mel_frames( + whisper_encoder::mel_frame_count_for_samples(pcm.size())); std::vector decoder_workspace(static_cast( - route::required_decoder_workspace_floats(pcm.size()))); - std::vector logits(static_cast(route::logits_size())); + whisper_decoder::required_workspace_floats(decoder_input_frames))); + std::vector logits(static_cast(whisper_decoder::vocab_size())); std::array generated_tokens = {}; std::array transcript = {}; int32_t transcript_size = -1; @@ -563,23 +568,36 @@ TEST_CASE("whisper_recognizer_runs_fixture_through_public_actor") { int32_t width = 0; uint64_t encoder_digest = 0u; uint64_t decoder_digest = 0u; - emel::error::type err = emel::error::cast(recognizer::error::none); - - recognizer::sm machine{}; - recognizer::event::initialize initialize_ev{ + emel::error::type err = emel::error::cast(transcriber::error::none); + + const transcriber::dependencies transcriber_deps{ + .encoder_kind = emel::speech::encoder::encoder_kind::whisper, + .decoder_kind = emel::speech::decoder::decoder_kind::whisper, + .tokenizer_kind = emel::speech::tokenizer::tokenizer_kind::whisper, + .encoder_contract = + whisper_encoder::bind_execution_contract(*loaded.model), + .decoder_contract = + whisper_decoder::bind_execution_contract(*loaded.model), + .decode_policy = + emel::speech::tokenizer::whisper::tiny_asr_decode_policy(), + .tokenizer_sha256 = + emel::speech::tokenizer::whisper::tiny_tokenizer_sha256(), + }; + transcriber::sm machine{transcriber_deps}; + transcriber::event::initialize initialize_ev{ *loaded.model, - recognizer::event::tokenizer_assets{ + transcriber::event::tokenizer_assets{ .model_json = tokenizer_json, .sha256 = emel::speech::tokenizer::whisper::tiny_tokenizer_sha256(), }}; initialize_ev.error_out = &err; REQUIRE(machine.process_event(initialize_ev)); - CHECK(err == emel::error::cast(recognizer::error::none)); + CHECK(err == emel::error::cast(transcriber::error::none)); - recognizer::event::recognize recognize_ev{ + transcriber::event::recognize recognize_ev{ *loaded.model, - recognizer::event::tokenizer_assets{ + transcriber::event::tokenizer_assets{ .model_json = tokenizer_json, .sha256 = emel::speech::tokenizer::whisper::tiny_tokenizer_sha256(), }, @@ -593,7 +611,7 @@ TEST_CASE("whisper_recognizer_runs_fixture_through_public_actor") { width, encoder_digest, decoder_digest}; - recognize_ev.storage = recognizer::event::runtime_storage{ + recognize_ev.storage = transcriber::event::runtime_storage{ .encoder_workspace = std::span{encoder_workspace}, .encoder_state = std::span{encoder_state}, .decoder_workspace = std::span{decoder_workspace}, @@ -603,7 +621,7 @@ TEST_CASE("whisper_recognizer_runs_fixture_through_public_actor") { recognize_ev.error_out = &err; REQUIRE(machine.process_event(recognize_ev)); - CHECK(err == emel::error::cast(recognizer::error::none)); + CHECK(err == emel::error::cast(transcriber::error::none)); CHECK(transcript_size >= 0); CHECK(selected_token > 0); CHECK(confidence >= 0.0f); diff --git a/tests/speech/recognizer/lifecycle_tests.cpp b/tests/speech/recognizer/lifecycle_tests.cpp deleted file mode 100644 index b8154a3b..00000000 --- a/tests/speech/recognizer/lifecycle_tests.cpp +++ /dev/null @@ -1,833 +0,0 @@ -#include "doctest/doctest.h" - -#include -#include -#include -#include -#include - -#include "emel/error/error.hpp" -#include "emel/model/data.hpp" -#include "emel/speech/recognizer/sm.hpp" - -namespace { - -namespace recognizer = emel::speech::recognizer; - -inline constexpr std::string_view k_backend_sha = "test-backend-sha"; - -struct initialize_error_capture { - int32_t calls = 0; - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); -}; - -struct initialize_done_capture { - int32_t calls = 0; - const emel::speech::recognizer::event::initialize *request = nullptr; -}; - -struct recognition_done_capture { - int32_t calls = 0; - const emel::speech::recognizer::event::recognize *request = nullptr; - int32_t transcript_size = 0; - int32_t selected_token = 0; - float confidence = 0.0f; - int32_t encoder_frame_count = 0; - int32_t encoder_width = 0; - int32_t generated_token_count = 0; - uint64_t encoder_digest = 0u; - uint64_t decoder_digest = 0u; -}; - -struct recognition_error_capture { - int32_t calls = 0; - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); -}; - -void record_initialize_done( - initialize_done_capture &capture, - const emel::speech::recognizer::events::initialize_done &ev) noexcept { - ++capture.calls; - capture.request = ev.request; -} - -void record_initialize_error( - initialize_error_capture &capture, - const emel::speech::recognizer::events::initialize_error &ev) noexcept { - ++capture.calls; - capture.err = ev.err; -} - -void record_recognition_done( - recognition_done_capture &capture, - const emel::speech::recognizer::events::recognition_done &ev) noexcept { - ++capture.calls; - capture.request = ev.request; - capture.transcript_size = ev.transcript_size; - capture.selected_token = ev.selected_token; - capture.confidence = ev.confidence; - capture.encoder_frame_count = ev.encoder_frame_count; - capture.encoder_width = ev.encoder_width; - capture.generated_token_count = ev.generated_token_count; - capture.encoder_digest = ev.encoder_digest; - capture.decoder_digest = ev.decoder_digest; -} - -void record_recognition_error( - recognition_error_capture &capture, - const emel::speech::recognizer::events::recognition_error &ev) noexcept { - ++capture.calls; - capture.err = ev.err; -} - -void noop_initialize_done( - const emel::speech::recognizer::events::initialize_done &) noexcept {} - -void noop_recognition_done( - const emel::speech::recognizer::events::recognition_done &) noexcept {} - -bool backend_supports_tokenizer( - const recognizer::event::tokenizer_assets &assets) noexcept { - return assets.model_json.data() != nullptr && !assets.model_json.empty() && - assets.sha256 == k_backend_sha; -} - -bool backend_supports_model(const emel::model::data &) noexcept { return true; } - -bool backend_recognition_ready( - const recognizer::event::recognize &request) noexcept { - return request.storage.encoder_workspace.data() != nullptr && - request.storage.encoder_state.data() != nullptr && - request.storage.decoder_workspace.data() != nullptr && - request.storage.logits.data() != nullptr && - request.storage.generated_tokens.data() != nullptr && - !request.storage.generated_tokens.empty(); -} - -void backend_encode(const recognizer::event::recognize &, - recognizer::event::recognize_ctx &ctx) noexcept { - ctx.encoder_accepted = true; - ctx.encoder_frame_count = 1; - ctx.encoder_width = 8; - ctx.encoder_digest = 1234u; - ctx.err = emel::error::cast(recognizer::error::none); -} - -void backend_decode(const recognizer::event::recognize &request, - recognizer::event::recognize_ctx &ctx) noexcept { - request.storage.generated_tokens[0] = 67; - ctx.generated_token_count = 1; - ctx.decoder_accepted = true; - ctx.selected_token = 67; - ctx.confidence = 0.875f; - ctx.decoder_digest = 5678u; - ctx.err = emel::error::cast(recognizer::error::none); -} - -void backend_detokenize(const recognizer::event::recognize &request, - recognizer::event::recognize_ctx &ctx) noexcept { - request.transcript[0] = 'O'; - request.transcript[1] = 'K'; - ctx.transcript_size = 2; - ctx.err = emel::error::cast(recognizer::error::none); -} - -struct guard_test_tokenizer_supported { - bool - operator()(const recognizer::event::tokenizer_assets &assets) const noexcept { - return backend_supports_tokenizer(assets); - } -}; - -struct guard_test_model_supported { - bool operator()(const emel::model::data &model) const noexcept { - return backend_supports_model(model); - } -}; - -struct guard_test_recognition_ready { - bool operator()(const recognizer::event::recognize &request) const noexcept { - return backend_recognition_ready(request); - } -}; - -struct effect_test_encode { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - backend_encode(runtime_ev.request, runtime_ev.ctx); - } -}; - -struct effect_test_decode { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - backend_decode(runtime_ev.request, runtime_ev.ctx); - } -}; - -struct effect_test_detokenize { - void operator()(const recognizer::event::recognize_run &runtime_ev, - recognizer::action::context &) const noexcept { - backend_detokenize(runtime_ev.request, runtime_ev.ctx); - } -}; - -struct test_route { - using guard_tokenizer_supported = guard_test_tokenizer_supported; - using guard_model_supported = guard_test_model_supported; - using guard_recognition_ready = guard_test_recognition_ready; - using effect_encode = effect_test_encode; - using effect_decode = effect_test_decode; - using effect_detokenize = effect_test_detokenize; -}; - -} // namespace - -TEST_CASE("speech_recognizer_rejects_invalid_initialize") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - initialize_error_capture capture{}; - - emel::speech::recognizer::event::initialize initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{}}; - initialize_ev.error_out = &err; - initialize_ev.on_error = emel::callback:: - from(&capture); - - CHECK_FALSE(recognizer.process_event(initialize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); - CHECK(capture.calls == 1); - CHECK(capture.err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); -} - -TEST_CASE("speech_recognizer_rejects_unsupported_initialized_model") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - - emel::speech::recognizer::event::initialize initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }}; - initialize_ev.error_out = &err; - - CHECK_FALSE(recognizer.process_event(initialize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::unsupported_model)); -} - -TEST_CASE("speech_recognizer_rejects_initialize_after_initialize_error") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - initialize_error_capture first_error{}; - initialize_error_capture second_error{}; - - emel::speech::recognizer::event::initialize invalid_initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{}}; - invalid_initialize_ev.error_out = &err; - invalid_initialize_ev.on_error = emel::callback:: - from(&first_error); - - CHECK_FALSE(recognizer.process_event(invalid_initialize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); - CHECK(first_error.calls == 1); - CHECK(recognizer.is(stateforward::sml::state)); - - err = emel::error::cast(emel::speech::recognizer::error::none); - emel::speech::recognizer::event::initialize valid_initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }}; - valid_initialize_ev.error_out = &err; - valid_initialize_ev.on_error = emel::callback:: - from(&second_error); - - CHECK_FALSE(recognizer.process_event(valid_initialize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); - CHECK(second_error.calls == 1); - CHECK(second_error.err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); - CHECK(recognizer.is(stateforward::sml::state)); -} - -TEST_CASE("speech_recognizer_runs_backend_route_from_public_events") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - initialize_done_capture initialize_capture{}; - - emel::speech::recognizer::event::initialize initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }}; - initialize_ev.error_out = &err; - initialize_ev.on_done = emel::callback:: - from( - &initialize_capture); - - REQUIRE(recognizer.process_event(initialize_ev)); - CHECK(initialize_capture.calls == 1); - CHECK(err == emel::error::cast(emel::speech::recognizer::error::none)); - - std::array pcm{}; - std::array encoder_workspace{}; - std::array encoder_state{}; - std::array decoder_workspace{}; - std::array logits{}; - std::array generated_tokens{}; - std::array transcript{}; - int32_t transcript_size = -1; - int32_t selected_token = -1; - float confidence = -1.0f; - int32_t encoder_frames = -1; - int32_t encoder_width = -1; - uint64_t encoder_digest = 1u; - uint64_t decoder_digest = 1u; - int32_t generated_token_count = -1; - recognition_done_capture recognition_capture{}; - - emel::speech::recognizer::event::recognize recognize_ev{ - *model, - emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }, - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - selected_token, - confidence, - encoder_frames, - encoder_width, - encoder_digest, - decoder_digest, - generated_token_count}; - recognize_ev.storage = emel::speech::recognizer::event::runtime_storage{ - .encoder_workspace = std::span{encoder_workspace}, - .encoder_state = std::span{encoder_state}, - .decoder_workspace = std::span{decoder_workspace}, - .logits = std::span{logits}, - .generated_tokens = std::span{generated_tokens}, - }; - recognize_ev.error_out = &err; - recognize_ev.on_done = emel::callback:: - from( - &recognition_capture); - - REQUIRE(recognizer.process_event(recognize_ev)); - CHECK(err == emel::error::cast(emel::speech::recognizer::error::none)); - CHECK(transcript_size == 2); - CHECK(transcript[0] == 'O'); - CHECK(transcript[1] == 'K'); - CHECK(selected_token == 67); - CHECK(confidence == doctest::Approx(0.875f)); - CHECK(encoder_frames == 1); - CHECK(encoder_width == 8); - CHECK(encoder_digest == 1234u); - CHECK(decoder_digest == 5678u); - CHECK(generated_token_count == 1); - CHECK(recognition_capture.calls == 1); - CHECK(recognition_capture.transcript_size == 2); - CHECK(recognition_capture.generated_token_count == 1); -} - -TEST_CASE("speech_recognizer_returns_to_ready_after_rejected_recognition") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - - emel::speech::recognizer::event::initialize initialize_ev{ - *model, emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }}; - initialize_ev.error_out = &err; - REQUIRE(recognizer.process_event(initialize_ev)); - CHECK(err == emel::error::cast(emel::speech::recognizer::error::none)); - - std::array invalid_transcript{}; - int32_t invalid_transcript_size = -1; - int32_t invalid_token = -1; - float invalid_confidence = -1.0f; - int32_t invalid_frames = -1; - int32_t invalid_width = -1; - uint64_t invalid_encoder_digest = 1u; - uint64_t invalid_decoder_digest = 1u; - recognition_error_capture error_capture{}; - - emel::speech::recognizer::event::recognize invalid_recognize_ev{ - *model, - emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }, - std::span{}, - 16000, - std::span{invalid_transcript}, - invalid_transcript_size, - invalid_token, - invalid_confidence, - invalid_frames, - invalid_width, - invalid_encoder_digest, - invalid_decoder_digest}; - invalid_recognize_ev.error_out = &err; - invalid_recognize_ev.on_error = emel::callback:: - from(&error_capture); - - CHECK_FALSE(recognizer.process_event(invalid_recognize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::invalid_request)); - CHECK(error_capture.calls == 1); - - std::array pcm{}; - std::array encoder_workspace{}; - std::array encoder_state{}; - std::array decoder_workspace{}; - std::array logits{}; - std::array generated_tokens{}; - std::array transcript{}; - int32_t transcript_size = -1; - int32_t selected_token = -1; - float confidence = -1.0f; - int32_t encoder_frames = -1; - int32_t encoder_width = -1; - uint64_t encoder_digest = 1u; - uint64_t decoder_digest = 1u; - int32_t generated_token_count = -1; - - emel::speech::recognizer::event::recognize valid_recognize_ev{ - *model, - emel::speech::recognizer::event::tokenizer_assets{ - .model_json = "{}", - .sha256 = k_backend_sha, - }, - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - selected_token, - confidence, - encoder_frames, - encoder_width, - encoder_digest, - decoder_digest, - generated_token_count}; - valid_recognize_ev.storage = emel::speech::recognizer::event::runtime_storage{ - .encoder_workspace = std::span{encoder_workspace}, - .encoder_state = std::span{encoder_state}, - .decoder_workspace = std::span{decoder_workspace}, - .logits = std::span{logits}, - .generated_tokens = std::span{generated_tokens}, - }; - err = emel::error::cast(emel::speech::recognizer::error::none); - valid_recognize_ev.error_out = &err; - - REQUIRE(recognizer.process_event(valid_recognize_ev)); - CHECK(err == emel::error::cast(emel::speech::recognizer::error::none)); - CHECK(transcript_size == 2); - CHECK(transcript[0] == 'O'); - CHECK(transcript[1] == 'K'); - CHECK(selected_token == 67); -} - -TEST_CASE("speech_recognizer_rejects_recognition_before_initialize") { - emel::speech::recognizer::sm recognizer{}; - auto model = std::make_unique(); - std::array pcm{}; - std::array transcript{}; - int32_t transcript_size = -1; - int32_t selected_token = -1; - float confidence = -1.0f; - int32_t encoder_frames = -1; - int32_t encoder_width = -1; - uint64_t encoder_digest = 1u; - uint64_t decoder_digest = 1u; - emel::error::type err = - emel::error::cast(emel::speech::recognizer::error::none); - recognition_error_capture capture{}; - - emel::speech::recognizer::event::recognize recognize_ev{ - *model, - "{}", - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - selected_token, - confidence, - encoder_frames, - encoder_width, - encoder_digest, - decoder_digest}; - recognize_ev.error_out = &err; - recognize_ev.on_error = emel::callback:: - from(&capture); - - CHECK_FALSE(recognizer.process_event(recognize_ev)); - CHECK(err == - emel::error::cast(emel::speech::recognizer::error::uninitialized)); - CHECK(capture.calls == 1); - CHECK(capture.err == - emel::error::cast(emel::speech::recognizer::error::uninitialized)); -} - -TEST_CASE("speech_recognizer_guards_keep_generic_boundary_explicit") { - namespace recognizer = emel::speech::recognizer; - - auto model = std::make_unique(); - recognizer::action::context ctx{}; - emel::error::type err = emel::error::cast(recognizer::error::none); - initialize_error_capture initialize_capture{}; - - recognizer::event::initialize invalid_initialize{ - *model, recognizer::event::tokenizer_assets{}}; - invalid_initialize.error_out = &err; - recognizer::event::initialize_ctx invalid_initialize_ctx{}; - recognizer::event::initialize_run invalid_initialize_run{ - invalid_initialize, invalid_initialize_ctx}; - - CHECK_FALSE( - recognizer::guard::guard_valid_initialize{}(invalid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_invalid_initialize{}(invalid_initialize_run, - ctx)); - CHECK_FALSE( - recognizer::guard::guard_initialize_tokenizer_supported{}( - invalid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_initialize_tokenizer_unsupported{}( - invalid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_has_initialize_error_out{}( - invalid_initialize_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_no_initialize_error_out{}( - invalid_initialize_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_has_initialize_done_callback{}( - invalid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_no_initialize_done_callback{}( - invalid_initialize_run, ctx)); - - recognizer::event::initialize valid_initialize{*model, "{}"}; - valid_initialize.tokenizer.sha256 = k_backend_sha; - valid_initialize.on_done = - emel::callback::from< - &noop_initialize_done>(); - valid_initialize.on_error = - emel::callback::from< - initialize_error_capture, record_initialize_error>( - &initialize_capture); - recognizer::event::initialize_ctx valid_initialize_ctx{}; - recognizer::event::initialize_run valid_initialize_run{valid_initialize, - valid_initialize_ctx}; - - CHECK(recognizer::guard::guard_valid_initialize{}(valid_initialize_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_invalid_initialize{}(valid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_initialize_tokenizer_supported{}( - valid_initialize_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_initialize_tokenizer_unsupported{}( - valid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_initialize_model_supported{}( - valid_initialize_run, ctx)); - CHECK(recognizer::guard:: - guard_initialize_model_supported_and_route_storage_ready< - test_route>{}(valid_initialize_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_initialize_unsupported_model{}( - valid_initialize_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_has_initialize_error_out{}( - valid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_no_initialize_error_out{}(valid_initialize_run, - ctx)); - CHECK(recognizer::guard::guard_has_initialize_done_callback{}( - valid_initialize_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_no_initialize_done_callback{}( - valid_initialize_run, ctx)); - CHECK(recognizer::guard::guard_has_initialize_error_callback{}( - valid_initialize_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_no_initialize_error_callback{}( - valid_initialize_run, ctx)); -} - -TEST_CASE("speech_recognizer_recognize_guards_stay_model_family_free") { - namespace recognizer = emel::speech::recognizer; - - auto model = std::make_unique(); - recognizer::action::context ctx{}; - std::array pcm{}; - std::array transcript{}; - int32_t transcript_size = -1; - int32_t selected_token = -1; - float confidence = -1.0f; - int32_t encoder_frames = -1; - int32_t encoder_width = -1; - uint64_t encoder_digest = 1u; - uint64_t decoder_digest = 1u; - emel::error::type err = emel::error::cast(recognizer::error::none); - recognition_error_capture recognition_capture{}; - - recognizer::event::recognize valid_recognize{*model, - "{}", - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - selected_token, - confidence, - encoder_frames, - encoder_width, - encoder_digest, - decoder_digest}; - valid_recognize.error_out = &err; - valid_recognize.on_done = - emel::callback::from< - &noop_recognition_done>(); - valid_recognize.on_error = - emel::callback::from< - recognition_error_capture, record_recognition_error>( - &recognition_capture); - recognizer::event::recognize_ctx valid_ctx{}; - recognizer::event::recognize_run valid_run{valid_recognize, valid_ctx}; - - CHECK(recognizer::guard::guard_valid_recognize{}(valid_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_invalid_recognize{}(valid_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_recognizer_route_ready{}( - valid_run, ctx)); - CHECK(recognizer::guard::guard_recognizer_route_unsupported{}( - valid_run, ctx)); - std::array encoder_workspace{}; - std::array encoder_state{}; - std::array decoder_workspace{}; - std::array logits{}; - std::array generated_tokens{}; - valid_recognize.storage = recognizer::event::runtime_storage{ - .encoder_workspace = std::span{encoder_workspace}, - .encoder_state = std::span{encoder_state}, - .decoder_workspace = std::span{decoder_workspace}, - .logits = std::span{logits}, - .generated_tokens = std::span{generated_tokens}, - }; - CHECK(recognizer::guard::guard_recognizer_route_ready{}(valid_run, - ctx)); - CHECK_FALSE( - recognizer::guard::guard_recognizer_route_unsupported{}( - valid_run, ctx)); - CHECK(recognizer::guard::guard_has_recognize_error_out{}(valid_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_no_recognize_error_out{}(valid_run, ctx)); - CHECK(recognizer::guard::guard_has_recognize_done_callback{}(valid_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_no_recognize_done_callback{}(valid_run, ctx)); - CHECK( - recognizer::guard::guard_has_recognize_error_callback{}(valid_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_no_recognize_error_callback{}(valid_run, ctx)); - - recognizer::event::recognize invalid_recognize{ - *model, "{}", std::span{}, - 16000, transcript, transcript_size, - selected_token, confidence, encoder_frames, - encoder_width, encoder_digest, decoder_digest}; - recognizer::event::recognize_ctx invalid_ctx{}; - recognizer::event::recognize_run invalid_run{invalid_recognize, invalid_ctx}; - - CHECK_FALSE(recognizer::guard::guard_valid_recognize{}(invalid_run, ctx)); - CHECK(recognizer::guard::guard_invalid_recognize{}(invalid_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_has_recognize_error_out{}(invalid_run, ctx)); - CHECK(recognizer::guard::guard_no_recognize_error_out{}(invalid_run, ctx)); - CHECK_FALSE( - recognizer::guard::guard_has_recognize_done_callback{}(invalid_run, ctx)); - CHECK( - recognizer::guard::guard_no_recognize_done_callback{}(invalid_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_has_recognize_error_callback{}( - invalid_run, ctx)); - CHECK( - recognizer::guard::guard_no_recognize_error_callback{}(invalid_run, ctx)); - - CHECK_FALSE(recognizer::guard::guard_encoder_success{}(valid_run, ctx)); - CHECK(recognizer::guard::guard_encoder_failure{}(valid_run, ctx)); - valid_ctx.encoder_accepted = true; - valid_ctx.err = emel::error::cast(recognizer::error::none); - CHECK(recognizer::guard::guard_encoder_success{}(valid_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_encoder_failure{}(valid_run, ctx)); - valid_ctx.err = emel::error::cast(recognizer::error::backend); - CHECK_FALSE(recognizer::guard::guard_encoder_success{}(valid_run, ctx)); - CHECK(recognizer::guard::guard_encoder_failure{}(valid_run, ctx)); - - valid_ctx.err = emel::error::cast(recognizer::error::none); - CHECK_FALSE(recognizer::guard::guard_decoder_success{}(valid_run, ctx)); - CHECK(recognizer::guard::guard_decoder_failure{}(valid_run, ctx)); - valid_ctx.decoder_accepted = true; - CHECK(recognizer::guard::guard_decoder_success{}(valid_run, ctx)); - CHECK_FALSE(recognizer::guard::guard_decoder_failure{}(valid_run, ctx)); -} - -TEST_CASE("speech_recognizer_actions_store_and_emit_dispatch_outputs") { - namespace recognizer = emel::speech::recognizer; - - auto model = std::make_unique(); - recognizer::action::context ctx{}; - emel::error::type err = emel::error::cast(recognizer::error::none); - initialize_done_capture initialize_done{}; - initialize_error_capture initialize_error{}; - - recognizer::event::initialize initialize_ev{*model, "{}"}; - initialize_ev.tokenizer.sha256 = k_backend_sha; - initialize_ev.error_out = &err; - initialize_ev.on_done = - emel::callback::from< - initialize_done_capture, record_initialize_done>(&initialize_done); - initialize_ev.on_error = - emel::callback::from< - initialize_error_capture, record_initialize_error>(&initialize_error); - recognizer::event::initialize_ctx initialize_ctx{}; - recognizer::event::initialize_run initialize_run{initialize_ev, - initialize_ctx}; - - recognizer::action::effect_begin_initialize(initialize_run, ctx); - CHECK(initialize_ctx.err == emel::error::cast(recognizer::error::none)); - recognizer::action::effect_reject_initialize(initialize_run, ctx); - CHECK(initialize_ctx.err == - emel::error::cast(recognizer::error::invalid_request)); - recognizer::action::effect_mark_tokenizer_invalid(initialize_run, ctx); - CHECK(initialize_ctx.err == - emel::error::cast(recognizer::error::tokenizer_invalid)); - recognizer::action::effect_mark_unsupported_model(initialize_run, ctx); - CHECK(initialize_ctx.err == - emel::error::cast(recognizer::error::unsupported_model)); - recognizer::action::effect_mark_initialize_backend_error(initialize_run, ctx); - CHECK(initialize_ctx.err == emel::error::cast(recognizer::error::backend)); - - recognizer::action::effect_store_initialize_error(initialize_run, ctx); - CHECK(err == emel::error::cast(recognizer::error::backend)); - initialize_ctx.err = emel::error::cast(recognizer::error::none); - recognizer::action::effect_store_initialize_success(initialize_run, ctx); - CHECK(err == emel::error::cast(recognizer::error::none)); - recognizer::action::effect_emit_initialize_done(initialize_run, ctx); - CHECK(initialize_done.calls == 1); - CHECK(initialize_done.request == &initialize_ev); - initialize_ctx.err = emel::error::cast(recognizer::error::backend); - recognizer::action::effect_emit_initialize_error(initialize_run, ctx); - CHECK(initialize_error.calls == 1); - CHECK(initialize_error.err == emel::error::cast(recognizer::error::backend)); - - std::array pcm{}; - std::array transcript{}; - int32_t transcript_size = -1; - int32_t selected_token = -1; - float confidence = -1.0f; - int32_t encoder_frames = -1; - int32_t encoder_width = -1; - uint64_t encoder_digest = 1u; - uint64_t decoder_digest = 1u; - int32_t generated_token_count = -1; - recognition_done_capture recognition_done{}; - recognition_error_capture recognition_error{}; - - recognizer::event::recognize recognize_ev{*model, - "{}", - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - selected_token, - confidence, - encoder_frames, - encoder_width, - encoder_digest, - decoder_digest, - generated_token_count}; - recognize_ev.error_out = &err; - recognize_ev.on_done = - emel::callback::from< - recognition_done_capture, record_recognition_done>(&recognition_done); - recognize_ev.on_error = - emel::callback::from< - recognition_error_capture, record_recognition_error>( - &recognition_error); - recognizer::event::recognize_ctx recognize_ctx{}; - recognizer::event::recognize_run recognize_run{recognize_ev, recognize_ctx}; - - recognizer::action::effect_begin_recognize(recognize_run, ctx); - CHECK(recognize_ctx.err == emel::error::cast(recognizer::error::none)); - CHECK_FALSE(recognize_ctx.encoder_accepted); - CHECK_FALSE(recognize_ctx.decoder_accepted); - CHECK(recognize_ctx.generated_token_count == 0); - CHECK(transcript_size == 0); - CHECK(generated_token_count == 0); - - recognizer::action::effect_reject_recognize(recognize_run, ctx); - CHECK(recognize_ctx.err == - emel::error::cast(recognizer::error::invalid_request)); - CHECK(transcript_size == 0); - recognizer::action::effect_mark_uninitialized(recognize_run, ctx); - CHECK(recognize_ctx.err == - emel::error::cast(recognizer::error::uninitialized)); - recognizer::action::effect_mark_backend_error(recognize_run, ctx); - CHECK(recognize_ctx.err == emel::error::cast(recognizer::error::backend)); - - recognize_ctx.selected_token = 42; - recognize_ctx.confidence = 0.875f; - recognize_ctx.encoder_frame_count = 7; - recognize_ctx.encoder_width = 384; - recognize_ctx.encoder_digest = 1234u; - recognize_ctx.decoder_digest = 5678u; - recognize_ctx.generated_token_count = 9; - recognize_ctx.transcript_size = 3; - recognizer::action::effect_publish_recognition_outputs(recognize_run, ctx); - CHECK(selected_token == 42); - CHECK(confidence == doctest::Approx(0.875f)); - CHECK(encoder_frames == 7); - CHECK(encoder_width == 384); - CHECK(encoder_digest == 1234u); - CHECK(decoder_digest == 5678u); - CHECK(generated_token_count == 9); - - recognize_ctx.err = emel::error::cast(recognizer::error::none); - recognizer::action::effect_store_recognize_success(recognize_run, ctx); - CHECK(err == emel::error::cast(recognizer::error::none)); - recognize_ctx.err = emel::error::cast(recognizer::error::backend); - recognizer::action::effect_store_recognize_error(recognize_run, ctx); - CHECK(err == emel::error::cast(recognizer::error::backend)); - - recognizer::action::effect_emit_recognize_done(recognize_run, ctx); - CHECK(recognition_done.calls == 1); - CHECK(recognition_done.request == &recognize_ev); - CHECK(recognition_done.transcript_size == 3); - CHECK(recognition_done.selected_token == 42); - CHECK(recognition_done.confidence == doctest::Approx(0.875f)); - CHECK(recognition_done.encoder_frame_count == 7); - CHECK(recognition_done.encoder_width == 384); - CHECK(recognition_done.generated_token_count == 9); - CHECK(recognition_done.encoder_digest == 1234u); - CHECK(recognition_done.decoder_digest == 5678u); - recognizer::action::effect_emit_recognize_error(recognize_run, ctx); - CHECK(recognition_error.calls == 1); - CHECK(recognition_error.err == emel::error::cast(recognizer::error::backend)); - - recognizer::action::effect_on_unexpected(0, ctx); -} diff --git a/tests/speech/transcriber/lifecycle_tests.cpp b/tests/speech/transcriber/lifecycle_tests.cpp new file mode 100644 index 00000000..6a665080 --- /dev/null +++ b/tests/speech/transcriber/lifecycle_tests.cpp @@ -0,0 +1,652 @@ +#include "doctest/doctest.h" + +#include +#include +#include +#include +#include + +#include "emel/error/error.hpp" +#include "emel/model/data.hpp" +#include "emel/speech/transcriber/sm.hpp" + +namespace { + +namespace transcriber = emel::speech::transcriber; + +inline constexpr std::string_view k_backend_sha = "test-backend-sha"; + +struct initialize_error_capture { + int32_t calls = 0; + emel::error::type err = emel::error::cast(transcriber::error::none); +}; + +struct initialize_done_capture { + int32_t calls = 0; + const transcriber::event::initialize *request = nullptr; +}; + +struct recognition_done_capture { + int32_t calls = 0; + const transcriber::event::recognize *request = nullptr; + int32_t transcript_size = 0; + int32_t selected_token = 0; + float confidence = 0.0f; + int32_t encoder_frame_count = 0; + int32_t encoder_width = 0; + int32_t generated_token_count = 0; + uint64_t encoder_digest = 0u; + uint64_t decoder_digest = 0u; +}; + +struct recognition_error_capture { + int32_t calls = 0; + emel::error::type err = emel::error::cast(transcriber::error::none); +}; + +void record_initialize_done( + initialize_done_capture &capture, + const transcriber::events::initialize_done &ev) noexcept { + ++capture.calls; + capture.request = ev.request; +} + +void record_initialize_error( + initialize_error_capture &capture, + const transcriber::events::initialize_error &ev) noexcept { + ++capture.calls; + capture.err = ev.err; +} + +void record_recognition_done( + recognition_done_capture &capture, + const transcriber::events::recognition_done &ev) noexcept { + ++capture.calls; + capture.request = ev.request; + capture.transcript_size = ev.transcript_size; + capture.selected_token = ev.selected_token; + capture.confidence = ev.confidence; + capture.encoder_frame_count = ev.encoder_frame_count; + capture.encoder_width = ev.encoder_width; + capture.generated_token_count = ev.generated_token_count; + capture.encoder_digest = ev.encoder_digest; + capture.decoder_digest = ev.decoder_digest; +} + +void record_recognition_error( + recognition_error_capture &capture, + const transcriber::events::recognition_error &ev) noexcept { + ++capture.calls; + capture.err = ev.err; +} + +// Synthetic injected dependencies: component-level kind selectors and contracts +// bound against a caller-owned model, mirroring what a variant binder would +// produce at the variant boundary. No variant headers or namespaces are used. +transcriber::dependencies +make_supported_dependencies(const emel::model::data &model) noexcept { + transcriber::dependencies deps{}; + deps.encoder_kind = emel::speech::encoder::encoder_kind::whisper; + deps.decoder_kind = emel::speech::decoder::decoder_kind::whisper; + deps.tokenizer_kind = emel::speech::tokenizer::tokenizer_kind::whisper; + deps.encoder_contract = emel::speech::encoder::execution_contract{ + .model = &model, + .sample_rate = 16000, + .mel_bin_count = 80, + .embedding_length = 384, + .feed_forward_length = 1536, + .attention_head_count = 6, + .encoder_block_count = 4, + }; + deps.decoder_contract = emel::speech::decoder::execution_contract{ + .model = &model, + .vocab_size = 51865, + .embedding_length = 384, + .decoder_block_count = 4, + }; + deps.decode_policy = emel::speech::tokenizer::asr_decode_policy{}; + deps.tokenizer_sha256 = k_backend_sha; + return deps; +} + +transcriber::event::tokenizer_assets make_supported_assets() noexcept { + return transcriber::event::tokenizer_assets{ + .model_json = "{}", + .sha256 = k_backend_sha, + }; +} + +} // namespace + +TEST_CASE("speech_transcriber_rejects_invalid_initialize") { + transcriber::sm engine{}; + auto model = std::make_unique(); + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_error_capture capture{}; + + transcriber::event::initialize initialize_ev{ + *model, transcriber::event::tokenizer_assets{}}; + initialize_ev.error_out = &err; + initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&capture); + + CHECK_FALSE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::invalid_request)); + CHECK(capture.calls == 1); + CHECK(capture.err == emel::error::cast(transcriber::error::invalid_request)); +} + +TEST_CASE("speech_transcriber_rejects_unsupported_tokenizer") { + transcriber::sm engine{}; + auto model = std::make_unique(); + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_error_capture capture{}; + + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&capture); + + CHECK_FALSE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::tokenizer_invalid)); + CHECK(capture.calls == 1); + CHECK(capture.err == + emel::error::cast(transcriber::error::tokenizer_invalid)); +} + +TEST_CASE("speech_transcriber_rejects_unsupported_initialized_model") { + auto model = std::make_unique(); + transcriber::dependencies deps{}; + deps.tokenizer_kind = emel::speech::tokenizer::tokenizer_kind::whisper; + deps.tokenizer_sha256 = k_backend_sha; + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + + CHECK_FALSE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::unsupported_model)); +} + +TEST_CASE("speech_transcriber_rejects_initialize_after_initialize_error") { + transcriber::sm engine{}; + auto model = std::make_unique(); + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_error_capture first_error{}; + initialize_error_capture second_error{}; + + transcriber::event::initialize invalid_initialize_ev{ + *model, transcriber::event::tokenizer_assets{}}; + invalid_initialize_ev.error_out = &err; + invalid_initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&first_error); + + CHECK_FALSE(engine.process_event(invalid_initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::invalid_request)); + CHECK(first_error.calls == 1); + CHECK(engine.is(stateforward::sml::state)); + + err = emel::error::cast(transcriber::error::none); + transcriber::event::initialize valid_initialize_ev{*model, + make_supported_assets()}; + valid_initialize_ev.error_out = &err; + valid_initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&second_error); + + CHECK_FALSE(engine.process_event(valid_initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::invalid_request)); + CHECK(second_error.calls == 1); + CHECK(second_error.err == + emel::error::cast(transcriber::error::invalid_request)); + CHECK(engine.is(stateforward::sml::state)); +} + +TEST_CASE("speech_transcriber_initializes_with_injected_contracts") { + auto model = std::make_unique(); + const transcriber::dependencies deps = make_supported_dependencies(*model); + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_done_capture done_capture{}; + + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + initialize_ev.on_done = + emel::callback::from< + initialize_done_capture, record_initialize_done>(&done_capture); + + REQUIRE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::none)); + CHECK(done_capture.calls == 1); + CHECK(done_capture.request == &initialize_ev); + CHECK(engine.is(stateforward::sml::state)); +} + +TEST_CASE("speech_transcriber_reports_backend_error_when_component_rejects") { + auto model = std::make_unique(); + const transcriber::dependencies deps = make_supported_dependencies(*model); + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + REQUIRE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::none)); + + std::array pcm{}; + std::array encoder_workspace{}; + std::array encoder_state{}; + std::array decoder_workspace{}; + std::array logits{}; + std::array generated_tokens{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + int32_t generated_token_count = -1; + recognition_error_capture error_capture{}; + + transcriber::event::recognize recognize_ev{*model, + make_supported_assets(), + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest, + generated_token_count}; + recognize_ev.storage = transcriber::event::runtime_storage{ + .encoder_workspace = std::span{encoder_workspace}, + .encoder_state = std::span{encoder_state}, + .decoder_workspace = std::span{decoder_workspace}, + .logits = std::span{logits}, + .generated_tokens = std::span{generated_tokens}, + }; + recognize_ev.error_out = &err; + recognize_ev.on_error = + emel::callback:: + from( + &error_capture); + + // The injected whisper-kind encoder component rejects the synthetic model + // (no tensors), so the pipeline reports a backend error and returns to + // ready. + CHECK_FALSE(engine.process_event(recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::backend)); + CHECK(error_capture.calls == 1); + CHECK(error_capture.err == emel::error::cast(transcriber::error::backend)); + CHECK(engine.is(stateforward::sml::state)); + + err = emel::error::cast(transcriber::error::none); + transcriber::event::initialize reinitialize_ev{*model, + make_supported_assets()}; + reinitialize_ev.error_out = &err; + CHECK(engine.process_event(reinitialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::none)); +} + +TEST_CASE("speech_transcriber_rejects_recognition_before_initialize") { + transcriber::sm engine{}; + auto model = std::make_unique(); + std::array pcm{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + emel::error::type err = emel::error::cast(transcriber::error::none); + recognition_error_capture capture{}; + + transcriber::event::recognize recognize_ev{*model, + "{}", + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest}; + recognize_ev.error_out = &err; + recognize_ev.on_error = + emel::callback:: + from(&capture); + + CHECK_FALSE(engine.process_event(recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::uninitialized)); + CHECK(capture.calls == 1); + CHECK(capture.err == emel::error::cast(transcriber::error::uninitialized)); +} + +TEST_CASE("speech_transcriber_returns_to_ready_after_rejected_recognition") { + auto model = std::make_unique(); + const transcriber::dependencies deps = make_supported_dependencies(*model); + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + REQUIRE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::none)); + + std::array invalid_transcript{}; + int32_t invalid_transcript_size = -1; + int32_t invalid_token = -1; + float invalid_confidence = -1.0f; + int32_t invalid_frames = -1; + int32_t invalid_width = -1; + uint64_t invalid_encoder_digest = 1u; + uint64_t invalid_decoder_digest = 1u; + recognition_error_capture error_capture{}; + + transcriber::event::recognize invalid_recognize_ev{ + *model, + make_supported_assets(), + std::span{}, + 16000, + std::span{invalid_transcript}, + invalid_transcript_size, + invalid_token, + invalid_confidence, + invalid_frames, + invalid_width, + invalid_encoder_digest, + invalid_decoder_digest}; + invalid_recognize_ev.error_out = &err; + invalid_recognize_ev.on_error = + emel::callback:: + from( + &error_capture); + + CHECK_FALSE(engine.process_event(invalid_recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::invalid_request)); + CHECK(error_capture.calls == 1); + CHECK(engine.is(stateforward::sml::state)); + + std::array pcm{}; + std::array encoder_workspace{}; + std::array encoder_state{}; + std::array decoder_workspace{}; + std::array logits{}; + std::array generated_tokens{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + int32_t generated_token_count = -1; + + transcriber::event::recognize valid_recognize_ev{*model, + make_supported_assets(), + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest, + generated_token_count}; + valid_recognize_ev.storage = transcriber::event::runtime_storage{ + .encoder_workspace = std::span{encoder_workspace}, + .encoder_state = std::span{encoder_state}, + .decoder_workspace = std::span{decoder_workspace}, + .logits = std::span{logits}, + .generated_tokens = std::span{generated_tokens}, + }; + err = emel::error::cast(transcriber::error::none); + valid_recognize_ev.error_out = &err; + + // The well-formed request still dispatches from ready (no unexpected-event + // path): it reaches the injected encoder component, which rejects the + // synthetic model with a backend error. + CHECK_FALSE(engine.process_event(valid_recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::backend)); + CHECK(engine.is(stateforward::sml::state)); +} + +TEST_CASE("speech_transcriber_guards_validate_injected_dependencies") { + auto model = std::make_unique(); + auto other_model = std::make_unique(); + const transcriber::dependencies supported = + make_supported_dependencies(*model); + + const transcriber::event::tokenizer_assets supported_assets = + make_supported_assets(); + CHECK(transcriber::guard::guard_tokenizer_assets_supported{}(supported_assets, + supported)); + + transcriber::dependencies unsupported_tokenizer = supported; + unsupported_tokenizer.tokenizer_kind = + emel::speech::tokenizer::tokenizer_kind::unsupported; + CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( + supported_assets, unsupported_tokenizer)); + + const transcriber::event::tokenizer_assets mismatched_sha_assets{ + .model_json = "{}", + .sha256 = "other-sha", + }; + CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( + mismatched_sha_assets, supported)); + + transcriber::dependencies empty_sha_deps = supported; + empty_sha_deps.tokenizer_sha256 = {}; + const transcriber::event::tokenizer_assets empty_sha_assets{ + .model_json = "{}", + .sha256 = {}, + }; + CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( + empty_sha_assets, empty_sha_deps)); + + const transcriber::event::tokenizer_assets empty_json_assets{ + .model_json = {}, + .sha256 = k_backend_sha, + }; + CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( + empty_json_assets, supported)); + + CHECK( + transcriber::guard::guard_model_contracts_supported{}(*model, supported)); + + transcriber::dependencies unsupported_encoder = supported; + unsupported_encoder.encoder_kind = + emel::speech::encoder::encoder_kind::unsupported; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, unsupported_encoder)); + + transcriber::dependencies unsupported_decoder = supported; + unsupported_decoder.decoder_kind = + emel::speech::decoder::decoder_kind::unsupported; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, unsupported_decoder)); + + // Contracts bound against a different model than the request carries. + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *other_model, supported)); + + transcriber::dependencies zero_encoder_embedding = supported; + zero_encoder_embedding.encoder_contract.embedding_length = 0; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, zero_encoder_embedding)); + + transcriber::dependencies zero_vocab = supported; + zero_vocab.decoder_contract.vocab_size = 0; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, zero_vocab)); + + transcriber::dependencies zero_decoder_embedding = supported; + zero_decoder_embedding.decoder_contract.embedding_length = 0; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, zero_decoder_embedding)); +} + +TEST_CASE("speech_transcriber_actions_store_and_emit_dispatch_outputs") { + auto model = std::make_unique(); + transcriber::action::context ctx{}; + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_done_capture initialize_done{}; + initialize_error_capture initialize_error{}; + + transcriber::event::initialize initialize_ev{*model, "{}"}; + initialize_ev.tokenizer.sha256 = k_backend_sha; + initialize_ev.error_out = &err; + initialize_ev.on_done = + emel::callback::from< + initialize_done_capture, record_initialize_done>(&initialize_done); + initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&initialize_error); + transcriber::event::initialize_ctx initialize_ctx{}; + transcriber::event::initialize_run initialize_run{initialize_ev, + initialize_ctx}; + + transcriber::action::effect_begin_initialize(initialize_run, ctx); + CHECK(initialize_ctx.err == emel::error::cast(transcriber::error::none)); + transcriber::action::effect_reject_initialize(initialize_run, ctx); + CHECK(initialize_ctx.err == + emel::error::cast(transcriber::error::invalid_request)); + transcriber::action::effect_mark_tokenizer_invalid(initialize_run, ctx); + CHECK(initialize_ctx.err == + emel::error::cast(transcriber::error::tokenizer_invalid)); + transcriber::action::effect_mark_unsupported_model(initialize_run, ctx); + CHECK(initialize_ctx.err == + emel::error::cast(transcriber::error::unsupported_model)); + transcriber::action::effect_mark_initialize_backend_error(initialize_run, + ctx); + CHECK(initialize_ctx.err == emel::error::cast(transcriber::error::backend)); + + transcriber::action::effect_store_initialize_error(initialize_run, ctx); + CHECK(err == emel::error::cast(transcriber::error::backend)); + initialize_ctx.err = emel::error::cast(transcriber::error::none); + transcriber::action::effect_store_initialize_success(initialize_run, ctx); + CHECK(err == emel::error::cast(transcriber::error::none)); + transcriber::action::effect_emit_initialize_done(initialize_run, ctx); + CHECK(initialize_done.calls == 1); + CHECK(initialize_done.request == &initialize_ev); + initialize_ctx.err = emel::error::cast(transcriber::error::backend); + transcriber::action::effect_emit_initialize_error(initialize_run, ctx); + CHECK(initialize_error.calls == 1); + CHECK(initialize_error.err == emel::error::cast(transcriber::error::backend)); + + std::array pcm{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + int32_t generated_token_count = -1; + recognition_done_capture recognition_done{}; + recognition_error_capture recognition_error{}; + + transcriber::event::recognize recognize_ev{*model, + "{}", + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest, + generated_token_count}; + recognize_ev.error_out = &err; + recognize_ev.on_done = + emel::callback::from< + recognition_done_capture, record_recognition_done>(&recognition_done); + recognize_ev.on_error = + emel::callback:: + from( + &recognition_error); + transcriber::event::recognize_ctx recognize_ctx{}; + transcriber::event::recognize_run recognize_run{recognize_ev, recognize_ctx}; + + transcriber::action::effect_begin_recognize(recognize_run, ctx); + CHECK(recognize_ctx.err == emel::error::cast(transcriber::error::none)); + CHECK_FALSE(recognize_ctx.encoder_accepted); + CHECK_FALSE(recognize_ctx.decoder_accepted); + CHECK_FALSE(recognize_ctx.detokenize_accepted); + CHECK(recognize_ctx.generated_token_count == 0); + CHECK(transcript_size == 0); + CHECK(generated_token_count == 0); + + transcriber::action::effect_reject_recognize(recognize_run, ctx); + CHECK(recognize_ctx.err == + emel::error::cast(transcriber::error::invalid_request)); + CHECK(transcript_size == 0); + transcriber::action::effect_mark_uninitialized(recognize_run, ctx); + CHECK(recognize_ctx.err == + emel::error::cast(transcriber::error::uninitialized)); + transcriber::action::effect_mark_backend_error(recognize_run, ctx); + CHECK(recognize_ctx.err == emel::error::cast(transcriber::error::backend)); + + recognize_ctx.selected_token = 42; + recognize_ctx.confidence = 0.875f; + recognize_ctx.encoder_frame_count = 7; + recognize_ctx.encoder_width = 384; + recognize_ctx.encoder_digest = 1234u; + recognize_ctx.decoder_digest = 5678u; + recognize_ctx.generated_token_count = 9; + recognize_ctx.transcript_size = 3; + recognize_ctx.detokenize_accepted = true; + transcriber::action::effect_publish_recognition_outputs(recognize_run, ctx); + CHECK(transcript_size == 3); + CHECK(selected_token == 42); + CHECK(confidence == doctest::Approx(0.875f)); + CHECK(encoder_frames == 7); + CHECK(encoder_width == 384); + CHECK(encoder_digest == 1234u); + CHECK(decoder_digest == 5678u); + CHECK(generated_token_count == 9); + + recognize_ctx.err = emel::error::cast(transcriber::error::none); + transcriber::action::effect_store_recognize_success(recognize_run, ctx); + CHECK(err == emel::error::cast(transcriber::error::none)); + recognize_ctx.err = emel::error::cast(transcriber::error::backend); + transcriber::action::effect_store_recognize_error(recognize_run, ctx); + CHECK(err == emel::error::cast(transcriber::error::backend)); + + transcriber::action::effect_emit_recognize_done(recognize_run, ctx); + CHECK(recognition_done.calls == 1); + CHECK(recognition_done.request == &recognize_ev); + CHECK(recognition_done.transcript_size == 3); + CHECK(recognition_done.selected_token == 42); + CHECK(recognition_done.confidence == doctest::Approx(0.875f)); + CHECK(recognition_done.encoder_frame_count == 7); + CHECK(recognition_done.encoder_width == 384); + CHECK(recognition_done.generated_token_count == 9); + CHECK(recognition_done.encoder_digest == 1234u); + CHECK(recognition_done.decoder_digest == 5678u); + transcriber::action::effect_emit_recognize_error(recognize_run, ctx); + CHECK(recognition_error.calls == 1); + CHECK(recognition_error.err == + emel::error::cast(transcriber::error::backend)); + + transcriber::action::effect_on_unexpected(0, ctx); +} diff --git a/tests/text/generator/detail_tests.cpp b/tests/text/generator/detail_tests.cpp index bade25ab..9d68266f 100644 --- a/tests/text/generator/detail_tests.cpp +++ b/tests/text/generator/detail_tests.cpp @@ -319,8 +319,8 @@ struct qwen3_runtime_fixture { add_matrix("blk.0.ffn_down.weight", 4, 4, std::vector(16, 0.0f)); add_matrix("blk.0.ffn_up.weight", 4, 4, std::vector(16, 0.0f)); model.n_tensors = tensor_index; - REQUIRE(emel::model::qwen3::detail::build_generation_contract(model, contract) == - emel::error::type{0}); + REQUIRE(emel::model::qwen3::detail::build_generation_contract( + model, contract) == emel::error::type{0}); } }; @@ -335,10 +335,10 @@ struct prepared_qwen3_backend_fixture { const auto runtime_policy = emel::text::generator::test::make_auto_runtime_policy( model_fixture.model); - ready = emel::text::generator::detail::prepare( - backend, model_fixture.contract, matmul.actor, - runtime_policy) == - emel::error::cast(emel::model::loader::error::none); + ready = + emel::text::generator::detail::prepare(backend, model_fixture.contract, + matmul.actor, runtime_policy) == + emel::error::cast(emel::model::loader::error::none); backend.bound_tokens = {0}; backend.bound_positions = {0}; backend.bound_token_count = 1; @@ -441,8 +441,8 @@ struct gemma4_runtime_fixture { add_matrix("blk.0.ffn_down.weight", 4, 4, std::vector(16, 0.0f)); add_matrix("blk.0.ffn_up.weight", 4, 4, std::vector(16, 0.0f)); model.n_tensors = tensor_index; - REQUIRE(emel::model::gemma4::detail::build_generation_contract(model, contract) == - emel::error::type{0}); + REQUIRE(emel::model::gemma4::detail::build_generation_contract( + model, contract) == emel::error::type{0}); } }; @@ -2385,8 +2385,8 @@ TEST_CASE("generator_detail_prepare_rejects_invalid_generation_contract") { emel::model::generation::contract contract = {}; emel::text::generator::runtime_policy runtime_policy = {}; - CHECK(emel::text::generator::detail::prepare( - backend, contract, matmul.actor, runtime_policy) == + CHECK(emel::text::generator::detail::prepare(backend, contract, matmul.actor, + runtime_policy) == emel::error::cast(emel::model::loader::error::model_invalid)); CHECK(backend.model == nullptr); } @@ -2407,8 +2407,8 @@ TEST_CASE("model_generation_validate_contract_rejects_incomplete_blocks") { const auto runtime_policy = emel::text::generator::test::make_auto_runtime_policy( model_fixture->model); - CHECK(emel::text::generator::detail::prepare( - backend, model_fixture->contract, matmul.actor, runtime_policy) == + CHECK(emel::text::generator::detail::prepare(backend, model_fixture->contract, + matmul.actor, runtime_policy) == emel::error::cast(emel::model::loader::error::model_invalid)); CHECK(backend.model == nullptr); } @@ -2423,10 +2423,10 @@ TEST_CASE("generator_detail_prepare_derives_kv_geometry_from_memory_contract") { model_fixture->model); // Default geometry: n_ctx=8 pads up to one 16-token block. - REQUIRE(emel::text::generator::detail::prepare( - *backend, model_fixture->contract, matmul.actor, - runtime_policy) == - emel::error::cast(emel::model::loader::error::none)); + REQUIRE( + emel::text::generator::detail::prepare(*backend, model_fixture->contract, + matmul.actor, runtime_policy) == + emel::error::cast(emel::model::loader::error::none)); CHECK(backend->kv_block_tokens == emel::memory::view::DEFAULT_BLOCK_TOKENS); CHECK(backend->kv_positions_capacity == emel::memory::view::DEFAULT_BLOCK_TOKENS); @@ -2443,10 +2443,10 @@ TEST_CASE("generator_detail_prepare_derives_kv_geometry_from_memory_contract") { // Non-positive block tokens cannot cover the context window and are // rejected through prepare's existing model_invalid validation path. - REQUIRE(emel::text::generator::detail::prepare(*backend, model_fixture->contract, - matmul.actor, runtime_policy, - 0) == - emel::error::cast(emel::model::loader::error::model_invalid)); + REQUIRE( + emel::text::generator::detail::prepare(*backend, model_fixture->contract, + matmul.actor, runtime_policy, 0) == + emel::error::cast(emel::model::loader::error::model_invalid)); // Non-divisible geometry: capacity rounds up to whole blocks. REQUIRE(emel::text::generator::detail::prepare( diff --git a/tools/bench/whisper_benchmark.py b/tools/bench/whisper_benchmark.py index 971b6102..facb8f62 100755 --- a/tools/bench/whisper_benchmark.py +++ b/tools/bench/whisper_benchmark.py @@ -20,8 +20,8 @@ WHISPER_CPP_REF = "v1.7.6" WHISPER_CPP_COMMIT = "a8d002cfd879315632a579e73f0148d06959de36" EXPECTED_TOKENIZER_SHA256 = "dfc530298b6fbed1a97c6472c575b026453706e2a204c7f7038f2c9d208b0759" -EMEL_BACKEND_ID = "emel.speech.recognizer.whisper" -EMEL_RUNTIME_SURFACE = "speech/recognizer+speech/recognizer_routes/whisper" +EMEL_BACKEND_ID = "emel.speech.transcriber.whisper" +EMEL_RUNTIME_SURFACE = "speech/transcriber+speech/encoder/whisper+speech/decoder/whisper+speech/tokenizer/whisper" def sha256_file(path: Path) -> str: diff --git a/tools/bench/whisper_benchmark_tests.cpp b/tools/bench/whisper_benchmark_tests.cpp index 8bf05643..e06b5df2 100644 --- a/tools/bench/whisper_benchmark_tests.cpp +++ b/tools/bench/whisper_benchmark_tests.cpp @@ -223,10 +223,8 @@ process_capture run_whisper_benchmark(const std::filesystem::path &tmp_dir, process_capture run_whisper_benchmark_defaults( const std::filesystem::path &tmp_dir, const std::filesystem::path &emel_model, - const std::filesystem::path &ref_model, - const std::string &emel_transcript, - const std::string &ref_transcript, - const std::string &extra_env = {}) { + const std::filesystem::path &ref_model, const std::string &emel_transcript, + const std::string &ref_transcript, const std::string &extra_env = {}) { const auto stdout_path = tmp_dir / "stdout.txt"; const auto stderr_path = tmp_dir / "stderr.txt"; const auto fake_emel = prepare_fake_runner(tmp_dir); @@ -290,7 +288,8 @@ process_capture run_whisper_compare(const std::filesystem::path &tmp_dir, } // namespace TEST_CASE("whisper emel parity runner stays on public runtime surfaces") { - const std::string source = read_file(whisper_emel_parity_runner_source_path()); + const std::string source = + read_file(whisper_emel_parity_runner_source_path()); REQUIRE(!source.empty()); CHECK(source.find("emel/model/whisper/detail.hpp") == std::string::npos); @@ -301,24 +300,23 @@ TEST_CASE("whisper emel parity runner stays on public runtime surfaces") { CHECK(source.find("emel/speech/tokenizer/whisper/detail.hpp") == std::string::npos); CHECK(source.find("emel/model/whisper/any.hpp") != std::string::npos); - CHECK(source.find("emel/speech/recognizer/sm.hpp") != std::string::npos); - CHECK(source.find("emel/speech/recognizer_routes/whisper/any.hpp") != - std::string::npos); - CHECK(source.find("emel/speech/encoder/whisper/sm.hpp") == - std::string::npos); - CHECK(source.find("emel/speech/decoder/whisper/sm.hpp") == + CHECK(source.find("emel/speech/transcriber/sm.hpp") != std::string::npos); + CHECK(source.find("emel/speech/encoder/whisper/any.hpp") != std::string::npos); - CHECK(source.find("emel::speech::encoder::whisper::sm") == - std::string::npos); - CHECK(source.find("emel::speech::decoder::whisper::sm") == + CHECK(source.find("emel/speech/decoder/whisper/any.hpp") != std::string::npos); + CHECK(source.find("emel/speech/encoder/whisper/sm.hpp") == std::string::npos); + CHECK(source.find("emel/speech/decoder/whisper/sm.hpp") == std::string::npos); + CHECK(source.find("emel::speech::encoder::whisper::sm") == std::string::npos); + CHECK(source.find("emel::speech::decoder::whisper::sm") == std::string::npos); CHECK(source.find("decode_token_ids") == std::string::npos); CHECK(source.find("emel/speech/tokenizer/whisper/any.hpp") != std::string::npos); } TEST_CASE("whisper emel parity runner escapes transcript JSON") { - const std::string source = read_file(whisper_emel_parity_runner_source_path()); + const std::string source = + read_file(whisper_emel_parity_runner_source_path()); REQUIRE(!source.empty()); CHECK(source.find("json_escape_string") != std::string::npos); @@ -357,7 +355,8 @@ TEST_CASE("whisper emel parity runner validates tensor name materialization") { "model_image)") != std::string::npos); } -TEST_CASE("whisper emel parity runner sizes transcript buffer from tokenizer vocab") { +TEST_CASE( + "whisper emel parity runner sizes transcript buffer from tokenizer vocab") { const std::string source = read_file(whisper_emel_parity_runner_source_path()); REQUIRE(!source.empty()); @@ -369,8 +368,7 @@ TEST_CASE("whisper emel parity runner sizes transcript buffer from tokenizer voc CHECK(source.find("k_transcript_bytes_per_token") == std::string::npos); CHECK(source.find("generated_tokens.size() * k_transcript_bytes_per_token") == std::string::npos); - CHECK(source.find("std::vector transcript(64u)") == - std::string::npos); + CHECK(source.find("std::vector transcript(64u)") == std::string::npos); } TEST_CASE("whisper normalizer rejects negative reader sizes") { @@ -404,8 +402,8 @@ TEST_CASE("whisper normalizer rejects negative reader sizes") { const std::string command = "python3 " + quote_arg_posix(script_path.string()) + " " + - quote_arg_posix(whisper_normalize_model_script_path().string()) + - " > " + quote_arg_posix(stdout_path.string()) + " 2> " + + quote_arg_posix(whisper_normalize_model_script_path().string()) + " > " + + quote_arg_posix(stdout_path.string()) + " 2> " + quote_arg_posix(stderr_path.string()); const auto capture = run_command_capture(command, stdout_path, stderr_path); CHECK(capture.exit_code == 0); @@ -515,8 +513,7 @@ TEST_CASE("whisper compare hard-fails transcript mismatch") { run_whisper_compare(tmp_dir, model, model, "[C]", "[Bell]"); CHECK(capture.exit_code == 1); CHECK(capture.stderr_text.empty()); - CHECK(capture.stdout_text.find("status=bounded_drift") != - std::string::npos); + CHECK(capture.stdout_text.find("status=bounded_drift") != std::string::npos); CHECK(capture.stdout_text.find("reason=transcript_mismatch") != std::string::npos); const std::string summary = read_file(tmp_dir / "out" / "summary.json"); @@ -545,10 +542,12 @@ TEST_CASE("whisper compare exact match succeeds") { CHECK(capture.stdout_text.find("status=exact_match") != std::string::npos); CHECK(capture.stdout_text.find("reason=ok") != std::string::npos); const std::string summary = read_file(tmp_dir / "out" / "summary.json"); - CHECK(summary.find("\"backend_id\": \"emel.speech.recognizer.whisper\"") != + CHECK(summary.find("\"backend_id\": \"emel.speech.transcriber.whisper\"") != + std::string::npos); + CHECK(summary.find( + "\"runtime_surface\": \"speech/transcriber+speech/encoder/whisper+" + "speech/decoder/whisper+speech/tokenizer/whisper\"") != std::string::npos); - CHECK(summary.find("\"runtime_surface\": \"speech/recognizer+speech/" - "recognizer_routes/whisper\"") != std::string::npos); #endif } @@ -711,9 +710,8 @@ TEST_CASE("whisper benchmark uses deterministic reference policy flags") { const auto model = tmp_dir / "model.bin"; write_text_file(model, "same-model"); - const auto capture = - run_whisper_benchmark(tmp_dir, model, model, "[C]", "[C]", - "REF_FAKE_SLEEP=0.2"); + const auto capture = run_whisper_benchmark(tmp_dir, model, model, "[C]", + "[C]", "REF_FAKE_SLEEP=0.2"); CHECK(capture.exit_code == 0); CHECK(capture.stderr_text.empty()); const std::string args = read_file(tmp_dir / "reference_args.txt"); @@ -723,9 +721,11 @@ TEST_CASE("whisper benchmark uses deterministic reference policy flags") { CHECK(args.find("--no-fallback\n") != std::string::npos); const std::string summary = read_file(tmp_dir / "out" / "benchmark_summary.json"); - CHECK(summary.find("\"backend_id\": \"emel.speech.recognizer.whisper\"") != + CHECK(summary.find("\"backend_id\": \"emel.speech.transcriber.whisper\"") != + std::string::npos); + CHECK(summary.find( + "\"runtime_surface\": \"speech/transcriber+speech/encoder/whisper+" + "speech/decoder/whisper+speech/tokenizer/whisper\"") != std::string::npos); - CHECK(summary.find("\"runtime_surface\": \"speech/recognizer+speech/" - "recognizer_routes/whisper\"") != std::string::npos); #endif } diff --git a/tools/bench/whisper_compare.py b/tools/bench/whisper_compare.py index fc34d284..d3c98aaa 100755 --- a/tools/bench/whisper_compare.py +++ b/tools/bench/whisper_compare.py @@ -14,8 +14,8 @@ SUMMARY_SCHEMA = "whisper_compare_summary/v1" COMPARE_GROUP = "whisper/tiny/q8_0/phase99_440hz_16khz_mono" EXPECTED_TOKENIZER_SHA256 = "dfc530298b6fbed1a97c6472c575b026453706e2a204c7f7038f2c9d208b0759" -EMEL_BACKEND_ID = "emel.speech.recognizer.whisper" -EMEL_RUNTIME_SURFACE = "speech/recognizer+speech/recognizer_routes/whisper" +EMEL_BACKEND_ID = "emel.speech.transcriber.whisper" +EMEL_RUNTIME_SURFACE = "speech/transcriber+speech/encoder/whisper+speech/decoder/whisper+speech/tokenizer/whisper" def sha256_file(path: Path) -> str: diff --git a/tools/bench/whisper_emel_parity_runner.cpp b/tools/bench/whisper_emel_parity_runner.cpp index 908ab0ed..a4aa8ede 100644 --- a/tools/bench/whisper_emel_parity_runner.cpp +++ b/tools/bench/whisper_emel_parity_runner.cpp @@ -29,9 +29,10 @@ #include "emel/model/detail.hpp" #include "emel/model/loader/errors.hpp" #include "emel/model/whisper/any.hpp" -#include "emel/speech/recognizer/sm.hpp" -#include "emel/speech/recognizer_routes/whisper/any.hpp" +#include "emel/speech/decoder/whisper/any.hpp" +#include "emel/speech/encoder/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/any.hpp" +#include "emel/speech/transcriber/sm.hpp" namespace { @@ -426,20 +427,30 @@ int main(int argc, char **argv) { const uint64_t binding_ns = elapsed_ns(binding_start, steady_clock::now()); const auto initialize_start = steady_clock::now(); - namespace speech_recognizer = emel::speech::recognizer; - namespace whisper_route = emel::speech::recognizer_routes::whisper; + namespace speech_transcriber = emel::speech::transcriber; + namespace whisper_decoder = emel::speech::decoder::whisper; + namespace whisper_encoder = emel::speech::encoder::whisper; namespace whisper_tokenizer = emel::speech::tokenizer::whisper; const auto &policy = whisper_tokenizer::tiny_asr_decode_policy(); - speech_recognizer::event::tokenizer_assets tokenizer_assets{ + speech_transcriber::event::tokenizer_assets tokenizer_assets{ .model_json = tokenizer_json, .sha256 = whisper_tokenizer::tiny_tokenizer_sha256(), }; - speech_recognizer::sm recognizer{}; + const speech_transcriber::dependencies transcriber_deps{ + .encoder_kind = emel::speech::encoder::encoder_kind::whisper, + .decoder_kind = emel::speech::decoder::decoder_kind::whisper, + .tokenizer_kind = emel::speech::tokenizer::tokenizer_kind::whisper, + .encoder_contract = whisper_encoder::bind_execution_contract(*model), + .decoder_contract = whisper_decoder::bind_execution_contract(*model), + .decode_policy = policy, + .tokenizer_sha256 = whisper_tokenizer::tiny_tokenizer_sha256(), + }; + speech_transcriber::sm transcriber{transcriber_deps}; emel::error::type initialize_err = - emel::error::cast(speech_recognizer::error::none); - speech_recognizer::event::initialize initialize_ev{*model, tokenizer_assets}; + emel::error::cast(speech_transcriber::error::none); + speech_transcriber::event::initialize initialize_ev{*model, tokenizer_assets}; initialize_ev.error_out = &initialize_err; - if (!recognizer.process_event(initialize_ev)) { + if (!transcriber.process_event(initialize_ev)) { std::fprintf(stderr, "error: failed to initialize Whisper runtime\n"); return 2; } @@ -448,14 +459,17 @@ int main(int argc, char **argv) { const auto recognize_start = steady_clock::now(); std::vector encoder_workspace(static_cast( - whisper_route::required_encoder_workspace_floats(pcm.size()))); + whisper_encoder::required_workspace_floats(pcm.size()))); std::vector encoder_state(static_cast( - whisper_route::required_encoder_state_floats(pcm.size()))); + whisper_encoder::required_encoder_output_floats(pcm.size()))); + const uint64_t encoder_frames = + whisper_encoder::encoder_frame_count_for_mel_frames( + whisper_encoder::mel_frame_count_for_samples(pcm.size())); std::vector decoder_workspace(static_cast( - whisper_route::required_decoder_workspace_floats(pcm.size()))); - std::vector logits(static_cast(whisper_route::logits_size())); + whisper_decoder::required_workspace_floats(encoder_frames))); + std::vector logits(static_cast(whisper_decoder::vocab_size())); std::vector generated_tokens( - static_cast(whisper_route::max_generated_token_count())); + static_cast(whisper_decoder::max_generated_token_count())); std::vector transcript(whisper_tokenizer::required_transcript_capacity( tokenizer_json, generated_tokens.size())); int32_t transcript_size = 0; @@ -467,21 +481,21 @@ int main(int argc, char **argv) { uint64_t decoder_digest = 0u; int32_t generated_token_count = 0; emel::error::type recognize_err = - emel::error::cast(speech_recognizer::error::none); - speech_recognizer::event::recognize recognize_ev{*model, - tokenizer_assets, - std::span{pcm}, - 16000, - std::span{transcript}, - transcript_size, - token, - confidence, - frames, - width, - encoder_digest, - decoder_digest, - generated_token_count}; - recognize_ev.storage = speech_recognizer::event::runtime_storage{ + emel::error::cast(speech_transcriber::error::none); + speech_transcriber::event::recognize recognize_ev{*model, + tokenizer_assets, + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + token, + confidence, + frames, + width, + encoder_digest, + decoder_digest, + generated_token_count}; + recognize_ev.storage = speech_transcriber::event::runtime_storage{ .encoder_workspace = std::span{encoder_workspace}, .encoder_state = std::span{encoder_state}, .decoder_workspace = std::span{decoder_workspace}, @@ -489,8 +503,8 @@ int main(int argc, char **argv) { .generated_tokens = std::span{generated_tokens}, }; recognize_ev.error_out = &recognize_err; - if (!recognizer.process_event(recognize_ev)) { - std::fprintf(stderr, "error: Whisper recognizer event failed\n"); + if (!transcriber.process_event(recognize_ev)) { + std::fprintf(stderr, "error: Whisper transcriber event failed\n"); return 2; } const uint64_t recognize_ns = @@ -522,11 +536,11 @@ int main(int argc, char **argv) { std::printf( "{\"schema\":\"whisper_compare/" "v1\",\"record_type\":\"result\",\"status\":\"ok\"," - "\"case_name\":\"emel/emel.speech.recognizer.whisper\"," + "\"case_name\":\"emel/emel.speech.transcriber.whisper\"," "\"compare_group\":\"whisper/tiny/q8_0/phase99_440hz_16khz_mono\"," - "\"lane\":\"emel\",\"backend_id\":\"emel.speech.recognizer.whisper\"," - "\"runtime_surface\":\"speech/recognizer+speech/recognizer_routes/" - "whisper\"," + "\"lane\":\"emel\",\"backend_id\":\"emel.speech.transcriber.whisper\"," + "\"runtime_surface\":\"speech/transcriber+speech/encoder/whisper+" + "speech/decoder/whisper+speech/tokenizer/whisper\"," "\"decode_policy_language\":\"%s\"," "\"decode_policy_task\":\"%s\"," "\"decode_policy_timestamp_mode\":\"%s\"," diff --git a/tools/docsgen/CMakeLists.txt b/tools/docsgen/CMakeLists.txt index b72c2615..6ff0f7a4 100644 --- a/tools/docsgen/CMakeLists.txt +++ b/tools/docsgen/CMakeLists.txt @@ -148,8 +148,11 @@ add_executable(docsgen docsgen.cpp "${GEN_HEADER}" ${GEN_SOURCES}) # The docs generator is an introspection utility, not runtime code. Release optimization makes # generated SML registration translation units dominate quality-gate latency without improving # the generated output. +# Large transition tables (e.g. speech/generator/moshi/executor) exceed clang's default +# 256-level bracket-depth limit when docsgen instantiates their full transition type lists. target_compile_options(docsgen PRIVATE "$<$:-O0;-g0;-fno-sanitize=all>" + "$<$:-fbracket-depth=1024>" "$<$:/Od>" ) diff --git a/tools/paritychecker/parity_engines.cpp b/tools/paritychecker/parity_engines.cpp index b1d6fb62..8db475f1 100644 --- a/tools/paritychecker/parity_engines.cpp +++ b/tools/paritychecker/parity_engines.cpp @@ -1014,6 +1014,8 @@ struct generation_load_state { emel::text::tokenizer::sm tokenizer = {}; emel::text::conditioner::sm conditioner = {}; emel::model::generation::contract generation_contract = {}; + emel::text::generator::matmul::lane_pool<7u, 128u, 1048576u> + parallel_matmul_lanes = {}; std::unique_ptr generator = {}; reference_backend reference = {}; generation_trace *emel_trace = nullptr; @@ -1095,8 +1097,8 @@ bool audit_has_native_quantized_tensor_type( const int32_t tensor_type = static_cast(dtype); for (const auto &stage : audit.stages) { if (stage.tensor_type == tensor_type && - stage.contract == - emel::model::generation::quantized_contract_kind::native_quantized) { + stage.contract == emel::model::generation::quantized_contract_kind:: + native_quantized) { return true; } } @@ -16204,8 +16206,8 @@ void dump_reference_decode_seam(generation_load_state &state) { const auto contract_name = emel::model::generation::quantized_contract_kind_name(stage.contract); const uint32_t supported = - stage.contract == - emel::model::generation::quantized_contract_kind::explicit_no_claim + stage.contract == emel::model::generation::quantized_contract_kind:: + explicit_no_claim ? 0u : 1u; std::fprintf( @@ -18930,7 +18932,7 @@ int run_generation_harness_contract( debug_audit)) { emel::model::generation::block_view debug_block{}; if (emel::model::generation::lookup_block_view(debug_execution, 0, - debug_block) == + debug_block) == emel::error::cast(emel::model::loader::error::none)) { const auto print_tensor_shape = [&](const char *label, From d72f26a496b32f02f81f14d00031ad8886716dad Mon Sep 17 00:00:00 2001 From: gabewillen Date: Fri, 10 Jul 2026 20:09:37 -0500 Subject: [PATCH 2/5] fix: harden transcriber component-kind and detokenize span guards Address PR review findings on the transcriber reshape: - Reject unknown component kinds before dispatch. The transcriber support guards only excluded the `unsupported` sentinel, but sm_any clamps any out-of-range enum value to the first variant, so a deserialized/cast kind with otherwise valid contracts would silently run the default facade. Add a variant-neutral `sm_any::is_supported_kind` predicate, expose it as `is_supported` on each speech component facade, and gate the transcriber guards on it so the engine stays model-family-neutral while rejecting out-of-range kinds. - Validate detokenize spans before decoding. The whisper tokenizer `guard_tokenizer_json_valid` accepted on JSON alone, so a direct component dispatch (bypassing the transcriber's recognize validation) with a non-empty token or transcript span whose data pointer is null would reach UB in the decode action. Extend the guard to require well-formed spans so malformed requests take the error path. - Remove the stray zero-byte a.out build artifact and ignore a.out/*.out. Add guard-level tests covering out-of-range component kinds and null-data detokenize spans. --- .gitignore | 4 ++ a.out | 0 src/emel/sm.hpp | 8 ++++ src/emel/speech/decoder/any.hpp | 8 ++++ src/emel/speech/encoder/any.hpp | 8 ++++ src/emel/speech/tokenizer/any.hpp | 8 ++++ src/emel/speech/tokenizer/whisper/guards.hpp | 11 +++++ src/emel/speech/transcriber/guards.hpp | 15 +++++-- tests/speech/tokenizer/whisper_tests.cpp | 47 ++++++++++++++++++++ tests/speech/transcriber/lifecycle_tests.cpp | 21 +++++++++ 10 files changed, 126 insertions(+), 4 deletions(-) delete mode 100755 a.out diff --git a/.gitignore b/.gitignore index 689a8a47..e6d4a78d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ tmp/profiles/ .gemini/ gha-creds-*.json +# Default compiler output binaries. +a.out +*.out + # Large scale-class generation fixtures are NOT committed to git. They are fetched # on demand from the provenance recorded in tests/models/README.md (source URL, pinned # repo commit, size, SHA256). See the Qwen3-4B-Q4_K_M.gguf README section. diff --git a/a.out b/a.out deleted file mode 100755 index e69de29b..00000000 diff --git a/src/emel/sm.hpp b/src/emel/sm.hpp index e0745c1b..afd7eaf2 100644 --- a/src/emel/sm.hpp +++ b/src/emel/sm.hpp @@ -1047,6 +1047,14 @@ class sm_any { kind_enum kind() const noexcept { return kind_; } + // A kind is supported only when it names a real variant. Out-of-range kinds + // (including the owner-defined `unsupported` sentinel) are clamped to the + // first variant by index_from_kind, so owners must reject them before + // dispatch instead of silently running the default variant. + static constexpr bool is_supported_kind(const kind_enum kind) noexcept { + return static_cast(kind) < k_sm_count; + } + template bool process_event(const event & ev) { static_assert(detail::type_list_contains::value, diff --git a/src/emel/speech/decoder/any.hpp b/src/emel/speech/decoder/any.hpp index 514b76b7..26a0b252 100644 --- a/src/emel/speech/decoder/any.hpp +++ b/src/emel/speech/decoder/any.hpp @@ -34,6 +34,14 @@ class any { decoder_kind kind() const noexcept { return core_.kind(); } + // True only for kinds that name a real variant; owners must reject every + // other value (including `unsupported` and out-of-range casts) before + // dispatch, since the facade would otherwise clamp them to the first variant. + static constexpr bool is_supported(const decoder_kind kind) noexcept { + return emel::sm_any::is_supported_kind( + kind); + } + bool process_event(const event::decode &ev) { return core_.process_event(ev); } diff --git a/src/emel/speech/encoder/any.hpp b/src/emel/speech/encoder/any.hpp index 20228e12..4b513aba 100644 --- a/src/emel/speech/encoder/any.hpp +++ b/src/emel/speech/encoder/any.hpp @@ -34,6 +34,14 @@ class any { encoder_kind kind() const noexcept { return core_.kind(); } + // True only for kinds that name a real variant; owners must reject every + // other value (including `unsupported` and out-of-range casts) before + // dispatch, since the facade would otherwise clamp them to the first variant. + static constexpr bool is_supported(const encoder_kind kind) noexcept { + return emel::sm_any::is_supported_kind( + kind); + } + bool process_event(const event::encode &ev) { return core_.process_event(ev); } diff --git a/src/emel/speech/tokenizer/any.hpp b/src/emel/speech/tokenizer/any.hpp index 4ddcd2f2..abb56694 100644 --- a/src/emel/speech/tokenizer/any.hpp +++ b/src/emel/speech/tokenizer/any.hpp @@ -34,6 +34,14 @@ class any { tokenizer_kind kind() const noexcept { return core_.kind(); } + // True only for kinds that name a real variant; owners must reject every + // other value (including `unsupported` and out-of-range casts) before + // dispatch, since the facade would otherwise clamp them to the first variant. + static constexpr bool is_supported(const tokenizer_kind kind) noexcept { + return emel::sm_any::is_supported_kind( + kind); + } + bool process_event(const event::detokenize &ev) { return core_.process_event(ev); } diff --git a/src/emel/speech/tokenizer/whisper/guards.hpp b/src/emel/speech/tokenizer/whisper/guards.hpp index c14442cb..b6b79811 100644 --- a/src/emel/speech/tokenizer/whisper/guards.hpp +++ b/src/emel/speech/tokenizer/whisper/guards.hpp @@ -7,10 +7,21 @@ namespace emel::speech::tokenizer::whisper::guard { +// The detokenize decision validates the whole request before any transcript +// bytes are written: the tokenizer JSON must carry the tiny control tokens, and +// the caller-owned spans must be well formed. The decode action writes through +// transcript.data() and iterates token_ids, so a direct component dispatch (via +// speech::tokenizer::any / whisper::sm, bypassing the transcriber's own +// recognize validation) with a non-empty span whose .data() is null would +// otherwise reach undefined behavior instead of a clean rejection. struct guard_tokenizer_json_valid { bool operator()(const event::detokenize_run &runtime_ev) const noexcept { return runtime_ev.request.tokenizer_json.data() != nullptr && !runtime_ev.request.tokenizer_json.empty() && + (runtime_ev.request.token_ids.empty() || + runtime_ev.request.token_ids.data() != nullptr) && + (runtime_ev.request.transcript.empty() || + runtime_ev.request.transcript.data() != nullptr) && detail::validate_tiny_control_tokens( runtime_ev.request.tokenizer_json); } diff --git a/src/emel/speech/transcriber/guards.hpp b/src/emel/speech/transcriber/guards.hpp index 0e7c73e8..0d54bd15 100644 --- a/src/emel/speech/transcriber/guards.hpp +++ b/src/emel/speech/transcriber/guards.hpp @@ -27,11 +27,18 @@ struct guard_invalid_initialize { // match the pinned checksum in the dependencies. Content-level validation // (model tensors, workspace capacity, tokenizer JSON control tokens) is owned // by the component machines and re-checked on every component dispatch. +// +// Component kinds are matched against the facade's supported-variant set, not +// merely against the `unsupported` sentinel: sm_any clamps any out-of-range +// enum value to the first variant, so an owner that accepted every +// non-`unsupported` value would silently run the default variant for a +// deserialized/cast kind it never meant to support. any::is_supported keeps +// that variant knowledge in the component facade, so the engine stays +// model-family-neutral. struct guard_tokenizer_assets_supported { bool operator()(const event::tokenizer_assets &assets, const dependencies &deps) const noexcept { - return deps.tokenizer_kind != - speech::tokenizer::tokenizer_kind::unsupported && + return speech::tokenizer::any::is_supported(deps.tokenizer_kind) && assets.model_json.data() != nullptr && !assets.model_json.empty() && !deps.tokenizer_sha256.empty() && assets.sha256 == deps.tokenizer_sha256; @@ -41,8 +48,8 @@ struct guard_tokenizer_assets_supported { struct guard_model_contracts_supported { bool operator()(const emel::model::data &model, const dependencies &deps) const noexcept { - return deps.encoder_kind != speech::encoder::encoder_kind::unsupported && - deps.decoder_kind != speech::decoder::decoder_kind::unsupported && + return speech::encoder::any::is_supported(deps.encoder_kind) && + speech::decoder::any::is_supported(deps.decoder_kind) && deps.encoder_contract.model == &model && deps.encoder_contract.embedding_length > 0 && deps.decoder_contract.model == &model && diff --git a/tests/speech/tokenizer/whisper_tests.cpp b/tests/speech/tokenizer/whisper_tests.cpp index 1254d5c8..2b35b4e5 100644 --- a/tests/speech/tokenizer/whisper_tests.cpp +++ b/tests/speech/tokenizer/whisper_tests.cpp @@ -8,6 +8,8 @@ #include "emel/speech/tokenizer/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/detail.hpp" +#include "emel/speech/tokenizer/whisper/events.hpp" +#include "emel/speech/tokenizer/whisper/guards.hpp" namespace { @@ -208,6 +210,51 @@ TEST_CASE("speech whisper tokenizer trims leading spaces and respects capacity") "ABell"); } +TEST_CASE("speech whisper tokenizer guard rejects malformed detokenize spans") { + namespace whisper = emel::speech::tokenizer::whisper; + + const auto tokenizer_path = + repo_root() / "tests" / "models" / "tokenizer-tiny.json"; + REQUIRE(std::filesystem::exists(tokenizer_path)); + const std::string tokenizer_json = read_text_file(tokenizer_path); + + int32_t token_ids[] = {whisper::detail::k_tiny_control_tokens.timestamp_begin}; + char transcript[16] = {}; + int32_t transcript_size = 0; + + // A well-formed request with valid JSON and valid spans passes the guard. + whisper::event::detokenize good_request{ + tokenizer_json, std::span{token_ids}, + std::span{transcript}, transcript_size}; + whisper::event::detokenize_ctx good_ctx{}; + whisper::event::detokenize_run good_run{good_request, good_ctx}; + CHECK(whisper::guard::guard_tokenizer_json_valid{}(good_run)); + CHECK_FALSE(whisper::guard::guard_tokenizer_json_invalid{}(good_run)); + + // A non-empty transcript span with a null data pointer must be rejected so the + // decode action never writes through the null pointer. + whisper::event::detokenize null_transcript_request{ + tokenizer_json, std::span{token_ids}, + std::span{static_cast(nullptr), 8u}, transcript_size}; + whisper::event::detokenize_ctx null_transcript_ctx{}; + whisper::event::detokenize_run null_transcript_run{null_transcript_request, + null_transcript_ctx}; + CHECK_FALSE(whisper::guard::guard_tokenizer_json_valid{}(null_transcript_run)); + CHECK(whisper::guard::guard_tokenizer_json_invalid{}(null_transcript_run)); + + // A non-empty token span with a null data pointer must be rejected so the + // decode action never iterates the null span. + whisper::event::detokenize null_tokens_request{ + tokenizer_json, + std::span{static_cast(nullptr), 4u}, + std::span{transcript}, transcript_size}; + whisper::event::detokenize_ctx null_tokens_ctx{}; + whisper::event::detokenize_run null_tokens_run{null_tokens_request, + null_tokens_ctx}; + CHECK_FALSE(whisper::guard::guard_tokenizer_json_valid{}(null_tokens_run)); + CHECK(whisper::guard::guard_tokenizer_json_invalid{}(null_tokens_run)); +} + TEST_CASE("speech whisper tokenizer clamps compacted output to capacity") { namespace whisper = emel::speech::tokenizer::whisper::detail; diff --git a/tests/speech/transcriber/lifecycle_tests.cpp b/tests/speech/transcriber/lifecycle_tests.cpp index 6a665080..f876353c 100644 --- a/tests/speech/transcriber/lifecycle_tests.cpp +++ b/tests/speech/transcriber/lifecycle_tests.cpp @@ -444,6 +444,14 @@ TEST_CASE("speech_transcriber_guards_validate_injected_dependencies") { CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( supported_assets, unsupported_tokenizer)); + // An out-of-range cast kind (neither `whisper` nor `unsupported`) must be + // rejected: sm_any would otherwise clamp it to the Whisper facade. + transcriber::dependencies unknown_tokenizer = supported; + unknown_tokenizer.tokenizer_kind = + static_cast(42); + CHECK_FALSE(transcriber::guard::guard_tokenizer_assets_supported{}( + supported_assets, unknown_tokenizer)); + const transcriber::event::tokenizer_assets mismatched_sha_assets{ .model_json = "{}", .sha256 = "other-sha", @@ -482,6 +490,19 @@ TEST_CASE("speech_transcriber_guards_validate_injected_dependencies") { CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( *model, unsupported_decoder)); + // Out-of-range cast component kinds must be rejected, not clamped to Whisper. + transcriber::dependencies unknown_encoder = supported; + unknown_encoder.encoder_kind = + static_cast(42); + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, unknown_encoder)); + + transcriber::dependencies unknown_decoder = supported; + unknown_decoder.decoder_kind = + static_cast(42); + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, unknown_decoder)); + // Contracts bound against a different model than the request carries. CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( *other_model, supported)); From bf44199ef1eecee07ef0269be603b305afda4d31 Mon Sep 17 00:00:00 2001 From: gabewillen Date: Fri, 10 Jul 2026 20:49:11 -0500 Subject: [PATCH 3/5] fix: preserve requested facade kinds and clear stale detokenize output Address second-round PR review findings: - Preserve the requested kind in sm_any. Constructing or set_kind-ing a component facade with `unsupported` or an out-of-range cast previously clamped kind() to the first variant, so the facade reported `whisper` and a direct dispatch silently ran the default machine with no way for callers to detect the "no component injected" state. sm_any now keeps the requested kind (the active machine still clamps for safe construction), so is_supported_kind(kind()) stays truthful and owners can reject dispatch to an unsupported facade. Facade comments document the owner-guard contract. - Clear transcript_size_out in the whisper detokenize begin action, mirroring the encoder/decoder begin actions, so rejected requests (invalid tokenizer JSON or malformed spans) never leave a caller reading a stale positive transcript size from a previous dispatch. Add sm_any kind-preservation tests, a tokenizer facade support test, and a machine-level test proving a rejected detokenize clears the caller's out-parameter. --- src/emel/sm.hpp | 13 +++- src/emel/speech/decoder/any.hpp | 7 +- src/emel/speech/encoder/any.hpp | 7 +- src/emel/speech/tokenizer/any.hpp | 7 +- src/emel/speech/tokenizer/whisper/actions.hpp | 10 ++- tests/sm/sm_any_tests.cpp | 25 ++++++++ tests/speech/tokenizer/whisper_tests.cpp | 64 +++++++++++++++++++ 7 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/emel/sm.hpp b/src/emel/sm.hpp index afd7eaf2..30653df1 100644 --- a/src/emel/sm.hpp +++ b/src/emel/sm.hpp @@ -1026,7 +1026,10 @@ template class sm_any { public: sm_any() { construct(default_index()); } - explicit sm_any(const kind_enum kind) { construct(index_from_kind(kind)); } + explicit sm_any(const kind_enum kind) { + construct(index_from_kind(kind)); + kind_ = kind; + } sm_any(const sm_any &) = delete; sm_any & operator=(const sm_any &) = delete; @@ -1041,10 +1044,16 @@ class sm_any { while (changed) { destroy(); construct(next); - return; + break; } + kind_ = kind; } + // Reports the kind the owner requested, even when it does not name a real + // variant. The active machine index is clamped to the first variant for such + // kinds (safe construction), but the requested kind is preserved so + // is_supported_kind(kind()) stays truthful and owners can reject dispatch to + // an unsupported facade instead of silently running the default variant. kind_enum kind() const noexcept { return kind_; } // A kind is supported only when it names a real variant. Out-of-range kinds diff --git a/src/emel/speech/decoder/any.hpp b/src/emel/speech/decoder/any.hpp index 26a0b252..69196117 100644 --- a/src/emel/speech/decoder/any.hpp +++ b/src/emel/speech/decoder/any.hpp @@ -10,9 +10,10 @@ namespace emel::speech::decoder { // Component variant selector. `unsupported` is a sentinel used by owners (e.g. // speech/transcriber dependencies) to model "no decoder injected"; owners must -// guard dispatch on a supported kind. sm_any clamps out-of-range kinds to the -// first variant, so constructing a facade with `unsupported` is safe as long as -// no event is dispatched to it. +// guard dispatch on is_supported(kind()). Constructing a facade with +// `unsupported` (or an out-of-range cast) is safe: the active machine clamps +// to the first variant, but kind() preserves the requested value so the guard +// stays truthful. No event may be dispatched to an unsupported facade. enum class decoder_kind : uint8_t { whisper = 0, unsupported = 255, diff --git a/src/emel/speech/encoder/any.hpp b/src/emel/speech/encoder/any.hpp index 4b513aba..1fe67352 100644 --- a/src/emel/speech/encoder/any.hpp +++ b/src/emel/speech/encoder/any.hpp @@ -10,9 +10,10 @@ namespace emel::speech::encoder { // Component variant selector. `unsupported` is a sentinel used by owners (e.g. // speech/transcriber dependencies) to model "no encoder injected"; owners must -// guard dispatch on a supported kind. sm_any clamps out-of-range kinds to the -// first variant, so constructing a facade with `unsupported` is safe as long as -// no event is dispatched to it. +// guard dispatch on is_supported(kind()). Constructing a facade with +// `unsupported` (or an out-of-range cast) is safe: the active machine clamps +// to the first variant, but kind() preserves the requested value so the guard +// stays truthful. No event may be dispatched to an unsupported facade. enum class encoder_kind : uint8_t { whisper = 0, unsupported = 255, diff --git a/src/emel/speech/tokenizer/any.hpp b/src/emel/speech/tokenizer/any.hpp index abb56694..bbf02ead 100644 --- a/src/emel/speech/tokenizer/any.hpp +++ b/src/emel/speech/tokenizer/any.hpp @@ -10,9 +10,10 @@ namespace emel::speech::tokenizer { // Component variant selector. `unsupported` is a sentinel used by owners (e.g. // speech/transcriber dependencies) to model "no tokenizer injected"; owners -// must guard dispatch on a supported kind. sm_any clamps out-of-range kinds to -// the first variant, so constructing a facade with `unsupported` is safe as -// long as no event is dispatched to it. +// must guard dispatch on is_supported(kind()). Constructing a facade with +// `unsupported` (or an out-of-range cast) is safe: the active machine clamps +// to the first variant, but kind() preserves the requested value so the guard +// stays truthful. No event may be dispatched to an unsupported facade. enum class tokenizer_kind : uint8_t { whisper = 0, unsupported = 255, diff --git a/src/emel/speech/tokenizer/whisper/actions.hpp b/src/emel/speech/tokenizer/whisper/actions.hpp index a3a72044..19441c74 100644 --- a/src/emel/speech/tokenizer/whisper/actions.hpp +++ b/src/emel/speech/tokenizer/whisper/actions.hpp @@ -11,8 +11,16 @@ namespace emel::speech::tokenizer::whisper::action { // The tokenizer machine holds no persistent actor-owned state. struct context {}; +// Clears the public outputs before validation, mirroring the encoder and +// decoder begin actions, so a rejected request never leaves a caller reading a +// stale transcript size from a previous dispatch. struct effect_begin_detokenize { - void operator()(const event::detokenize_run &, context &) const noexcept {} + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::none); + runtime_ev.ctx.transcript_size = 0; + runtime_ev.request.transcript_size_out = 0; + } }; struct effect_detokenize { diff --git a/tests/sm/sm_any_tests.cpp b/tests/sm/sm_any_tests.cpp index e6c96872..59b2e54d 100644 --- a/tests/sm/sm_any_tests.cpp +++ b/tests/sm/sm_any_tests.cpp @@ -129,3 +129,28 @@ TEST_CASE("sm_any_normalizes_invalid_kind_to_default") { any.process_event(event_alpha{&value}); CHECK(value == 11); } + +TEST_CASE("sm_any_preserves_requested_kind_and_reports_support") { + static_assert(any_sm::is_supported_kind(test_kind::first)); + static_assert(any_sm::is_supported_kind(test_kind::second)); + static_assert(!any_sm::is_supported_kind(static_cast(42))); + static_assert(!any_sm::is_supported_kind(static_cast(255))); + + // The active machine clamps to the default variant, but kind() preserves the + // requested value so owners can reject dispatch to an unsupported facade. + any_sm constructed_invalid{static_cast(42)}; + CHECK(constructed_invalid.kind() == static_cast(42)); + CHECK_FALSE(any_sm::is_supported_kind(constructed_invalid.kind())); + + any_sm reassigned{test_kind::second}; + CHECK(reassigned.kind() == test_kind::second); + reassigned.set_kind(static_cast(255)); + CHECK(reassigned.kind() == static_cast(255)); + CHECK_FALSE(any_sm::is_supported_kind(reassigned.kind())); + + reassigned.set_kind(test_kind::first); + CHECK(reassigned.kind() == test_kind::first); + int32_t value = 0; + reassigned.process_event(event_alpha{&value}); + CHECK(value == 11); +} diff --git a/tests/speech/tokenizer/whisper_tests.cpp b/tests/speech/tokenizer/whisper_tests.cpp index 2b35b4e5..0cb4a892 100644 --- a/tests/speech/tokenizer/whisper_tests.cpp +++ b/tests/speech/tokenizer/whisper_tests.cpp @@ -6,10 +6,13 @@ #include #include +#include "emel/error/error.hpp" +#include "emel/speech/tokenizer/any.hpp" #include "emel/speech/tokenizer/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/detail.hpp" #include "emel/speech/tokenizer/whisper/events.hpp" #include "emel/speech/tokenizer/whisper/guards.hpp" +#include "emel/speech/tokenizer/whisper/sm.hpp" namespace { @@ -255,6 +258,67 @@ TEST_CASE("speech whisper tokenizer guard rejects malformed detokenize spans") { CHECK(whisper::guard::guard_tokenizer_json_invalid{}(null_tokens_run)); } +TEST_CASE("speech whisper tokenizer clears stale transcript size on rejection") { + namespace tokenizer = emel::speech::tokenizer; + + const auto tokenizer_path = + repo_root() / "tests" / "models" / "tokenizer-tiny.json"; + REQUIRE(std::filesystem::exists(tokenizer_path)); + const std::string tokenizer_json = read_text_file(tokenizer_path); + + tokenizer::whisper::sm machine{}; + int32_t token_ids[] = {542, 33, 898, 60}; + char transcript[16] = {}; + int32_t transcript_size = -1; + emel::error::type err = + emel::error::cast(tokenizer::whisper::error::internal_error); + + tokenizer::event::detokenize good_request{ + tokenizer_json, std::span{token_ids}, + std::span{transcript}, transcript_size}; + good_request.error_out = &err; + REQUIRE(machine.process_event(good_request)); + CHECK(err == emel::error::cast(tokenizer::whisper::error::none)); + REQUIRE(transcript_size > 0); + + // A rejected request must clear the caller's out-parameter instead of + // leaving the previous dispatch's positive size next to a failure result. + tokenizer::event::detokenize bad_request{"{}", + std::span{token_ids}, + std::span{transcript}, + transcript_size}; + bad_request.error_out = &err; + CHECK_FALSE(machine.process_event(bad_request)); + CHECK(err == + emel::error::cast(tokenizer::whisper::error::tokenizer_json_invalid)); + CHECK(transcript_size == 0); +} + +TEST_CASE("speech tokenizer facade preserves unsupported kinds") { + namespace tokenizer = emel::speech::tokenizer; + + static_assert( + tokenizer::any::is_supported(tokenizer::tokenizer_kind::whisper)); + static_assert( + !tokenizer::any::is_supported(tokenizer::tokenizer_kind::unsupported)); + static_assert( + !tokenizer::any::is_supported(static_cast(42))); + + // kind() reports the requested value so owners can reject dispatch to an + // unsupported facade instead of silently running the default variant. + tokenizer::any unsupported_facade{tokenizer::tokenizer_kind::unsupported}; + CHECK(unsupported_facade.kind() == tokenizer::tokenizer_kind::unsupported); + CHECK_FALSE(tokenizer::any::is_supported(unsupported_facade.kind())); + + tokenizer::any cast_facade{static_cast(42)}; + CHECK(cast_facade.kind() == static_cast(42)); + CHECK_FALSE(tokenizer::any::is_supported(cast_facade.kind())); + + cast_facade.set_kind(tokenizer::tokenizer_kind::whisper); + CHECK(cast_facade.kind() == tokenizer::tokenizer_kind::whisper); + CHECK(tokenizer::any::is_supported(cast_facade.kind())); +} + TEST_CASE("speech whisper tokenizer clamps compacted output to capacity") { namespace whisper = emel::speech::tokenizer::whisper::detail; From 10d00c897f94a9481eeb7d5224232530dca86c2b Mon Sep 17 00:00:00 2001 From: gabewillen Date: Fri, 10 Jul 2026 22:54:18 -0500 Subject: [PATCH 4/5] fix: keep failed pre-init recognizes out of ready Address the P1 Codex review finding: recognize dispatched from state_uninitialized or state_errored was marked uninitialized but routed through the shared recognize error chain, whose terminals always land in state_ready. A failed pre-initialize recognize therefore promoted the machine to ready, and the next recognize could pass guard_transcriber_ready and run the encoder/decoder/tokenizer pipeline without any successful initialize. Give each pre-initialize origin its own explicit rejection chain (uninitialized/errored error-out and error-callback decision states) whose terminals return to the origin state, reusing the existing error-out/callback guards and effects. The shared recognize error chain is now reachable only from ready-origin dispatches and correctly returns to ready. Origin awareness is modeled as distinct states per the actor rules, not context flags. Also address two unthreaded Copilot review findings on the same surface: - guard_model_contracts_supported now rejects injected contracts whose encoder and decoder embedding lengths disagree, since the decode phase slices the shared encoder-state buffer as frame_count x decoder embedding_length. - guard_encoder_success now requires that slice to fit the caller's encoder_state span (and a non-negative frame count), so effect_decode can never hand the decoder a span that reads past the caller's buffer; oversized outcomes route to the backend-error path. Add lifecycle tests proving pre-init and post-error recognizes reject and stay in their origin states with the pipeline unreachable, plus guard tests for the embedding mismatch and capacity bound. --- .../mermaid/speech_transcriber.mmd | 12 +- .planning/architecture/speech_transcriber.md | 24 ++- src/emel/speech/transcriber/guards.hpp | 25 ++- src/emel/speech/transcriber/sm.hpp | 54 +++++- tests/speech/transcriber/lifecycle_tests.cpp | 163 +++++++++++++++++- 5 files changed, 266 insertions(+), 12 deletions(-) diff --git a/.planning/architecture/mermaid/speech_transcriber.mmd b/.planning/architecture/mermaid/speech_transcriber.mmd index 6627cf77..d5137287 100644 --- a/.planning/architecture/mermaid/speech_transcriber.mmd +++ b/.planning/architecture/mermaid/speech_transcriber.mmd @@ -21,8 +21,8 @@ stateDiagram-v2 state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_no_initialize_error_callback_] / none state_ready --> state_recognize_support_decision : recognize_run [guard_valid_recognize_] / effect_begin_recognize_ state_ready --> state_recognize_error_out_decision : recognize_run [guard_invalid_recognize_] / effect_reject_recognize_ - state_uninitialized --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ - state_errored --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_uninitialized --> state_recognize_uninitialized_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_errored --> state_recognize_errored_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ state_recognize_support_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_transcriber_unsupported_] / effect_mark_uninitialized_ state_recognize_support_decision --> state_encoding : completion_recognize_run_ [guard_transcriber_ready_] / effect_encode_ state_encoding --> state_encoder_decision : completion_recognize_run_ [always] / none @@ -43,6 +43,14 @@ stateDiagram-v2 state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_no_recognize_error_callback_] / none state_done --> state_ready : completion_recognize_run_ [always] / none + state_recognize_uninitialized_error_out_decision --> state_recognize_uninitialized_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_uninitialized_error_out_decision --> state_recognize_uninitialized_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_uninitialized_error_callback_decision --> state_uninitialized : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_uninitialized_error_callback_decision --> state_uninitialized : completion_recognize_run_ [guard_no_recognize_error_callback_] / none + state_recognize_errored_error_out_decision --> state_recognize_errored_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_errored_error_out_decision --> state_recognize_errored_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_errored_error_callback_decision --> state_errored : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_errored_error_callback_decision --> state_errored : completion_recognize_run_ [guard_no_recognize_error_callback_] / none state_uninitialized --> state_uninitialized : _ [always] / effect_on_unexpected_ state_ready --> state_ready : _ [always] / effect_on_unexpected_ state_errored --> state_errored : _ [always] / effect_on_unexpected_ diff --git a/.planning/architecture/speech_transcriber.md b/.planning/architecture/speech_transcriber.md index 55f21af3..a4d67291 100644 --- a/.planning/architecture/speech_transcriber.md +++ b/.planning/architecture/speech_transcriber.md @@ -28,8 +28,8 @@ stateDiagram-v2 state_initialize_error_callback_decision --> state_errored : completion_initialize_run_ [guard_no_initialize_error_callback_] / none state_ready --> state_recognize_support_decision : recognize_run [guard_valid_recognize_] / effect_begin_recognize_ state_ready --> state_recognize_error_out_decision : recognize_run [guard_invalid_recognize_] / effect_reject_recognize_ - state_uninitialized --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ - state_errored --> state_recognize_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_uninitialized --> state_recognize_uninitialized_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ + state_errored --> state_recognize_errored_error_out_decision : recognize_run [always] / effect_mark_uninitialized_ state_recognize_support_decision --> state_recognize_error_out_decision : completion_recognize_run_ [guard_transcriber_unsupported_] / effect_mark_uninitialized_ state_recognize_support_decision --> state_encoding : completion_recognize_run_ [guard_transcriber_ready_] / effect_encode_ state_encoding --> state_encoder_decision : completion_recognize_run_ [always] / none @@ -50,6 +50,14 @@ stateDiagram-v2 state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ state_recognize_error_callback_decision --> state_ready : completion_recognize_run_ [guard_no_recognize_error_callback_] / none state_done --> state_ready : completion_recognize_run_ [always] / none + state_recognize_uninitialized_error_out_decision --> state_recognize_uninitialized_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_uninitialized_error_out_decision --> state_recognize_uninitialized_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_uninitialized_error_callback_decision --> state_uninitialized : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_uninitialized_error_callback_decision --> state_uninitialized : completion_recognize_run_ [guard_no_recognize_error_callback_] / none + state_recognize_errored_error_out_decision --> state_recognize_errored_error_callback_decision : completion_recognize_run_ [guard_has_recognize_error_out_] / effect_store_recognize_error_ + state_recognize_errored_error_out_decision --> state_recognize_errored_error_callback_decision : completion_recognize_run_ [guard_no_recognize_error_out_] / none + state_recognize_errored_error_callback_decision --> state_errored : completion_recognize_run_ [guard_has_recognize_error_callback_] / effect_emit_recognize_error_ + state_recognize_errored_error_callback_decision --> state_errored : completion_recognize_run_ [guard_no_recognize_error_callback_] / none state_uninitialized --> state_uninitialized : _ [always] / effect_on_unexpected_ state_ready --> state_ready : _ [always] / effect_on_unexpected_ state_errored --> state_errored : _ [always] / effect_on_unexpected_ @@ -79,8 +87,8 @@ stateDiagram-v2 | [`state_initialize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_initialize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_valid_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_begin_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_invalid_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_recognize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | -| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | -| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_uninitialized_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`recognize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_errored_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_transcriber_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_uninitialized>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_recognize_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_transcriber_ready>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_encode>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_encoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_encoding`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_encoder_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | @@ -101,6 +109,14 @@ stateDiagram-v2 | [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_recognize_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_uninitialized_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_uninitialized_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_uninitialized_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_uninitialized_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_uninitialized_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_uninitialized_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_errored_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_errored_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_errored_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_recognize_errored_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_errored_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_emit_recognize_error>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_recognize_errored_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_no_recognize_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_uninitialized`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | diff --git a/src/emel/speech/transcriber/guards.hpp b/src/emel/speech/transcriber/guards.hpp index 0d54bd15..962ae35a 100644 --- a/src/emel/speech/transcriber/guards.hpp +++ b/src/emel/speech/transcriber/guards.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "emel/speech/transcriber/context.hpp" #include "emel/speech/transcriber/detail.hpp" #include "emel/speech/transcriber/events.hpp" @@ -45,6 +47,11 @@ struct guard_tokenizer_assets_supported { } }; +// The encoder output feeds the decoder cross-attention directly, and the +// decode phase slices the shared encoder-state buffer as frame_count x +// decoder embedding_length, so injected contracts whose embedding lengths +// disagree would make the pipeline mis-slice the buffer; they are rejected +// here as an unsupported dependency combination. struct guard_model_contracts_supported { bool operator()(const emel::model::data &model, const dependencies &deps) const noexcept { @@ -54,7 +61,9 @@ struct guard_model_contracts_supported { deps.encoder_contract.embedding_length > 0 && deps.decoder_contract.model == &model && deps.decoder_contract.vocab_size > 0 && - deps.decoder_contract.embedding_length > 0; + deps.decoder_contract.embedding_length > 0 && + deps.encoder_contract.embedding_length == + deps.decoder_contract.embedding_length; } }; @@ -164,11 +173,21 @@ struct guard_transcriber_unsupported { } }; +// The decode phase reads frame_count x decoder embedding_length floats from +// the caller-owned encoder-state buffer, so the encode outcome only counts as +// a success when that slice provably fits the buffer the caller provided; +// otherwise the pipeline takes the backend-error path instead of letting the +// decoder read past the span. struct guard_encoder_success { bool operator()(const event::recognize_run &runtime_ev, - const action::context &) const noexcept { + const action::context &ctx) const noexcept { return runtime_ev.ctx.encoder_accepted && - runtime_ev.ctx.err == detail::to_error(error::none); + runtime_ev.ctx.err == detail::to_error(error::none) && + runtime_ev.ctx.encoder_frame_count >= 0 && + static_cast(runtime_ev.ctx.encoder_frame_count) * + static_cast( + ctx.deps.decoder_contract.embedding_length) <= + runtime_ev.request.storage.encoder_state.size(); } }; diff --git a/src/emel/speech/transcriber/sm.hpp b/src/emel/speech/transcriber/sm.hpp index ebc8b550..f934242d 100644 --- a/src/emel/speech/transcriber/sm.hpp +++ b/src/emel/speech/transcriber/sm.hpp @@ -31,6 +31,10 @@ struct state_recognize_success {}; struct state_recognize_error_out_decision {}; struct state_recognize_done_callback_decision {}; struct state_recognize_error_callback_decision {}; +struct state_recognize_uninitialized_error_out_decision {}; +struct state_recognize_uninitialized_error_callback_decision {}; +struct state_recognize_errored_error_out_decision {}; +struct state_recognize_errored_error_callback_decision {}; struct state_done {}; struct state_errored {}; @@ -46,6 +50,12 @@ state purpose speech/tokenizer facades); each phase outcome is an explicit decision state. - done/error error-out and callback decision states keep the error_out and on_done/on_error channels as explicit transitions. +- recognize rejections dispatched before a successful initialize have their own + origin-specific error chains (uninitialized/errored error-out and callback + decisions) whose terminals return to the origin state, so a rejected + pre-initialize recognize can never promote the machine to ready and open the + pipeline without a successful initialize. The shared recognize error chain is + reachable only from ready-origin dispatches and correctly returns to ready. control invariants - the machine contains no model-family names, contracts, routes, constants, or @@ -128,10 +138,11 @@ struct model { , sml::state <= sml::state + sml::event [ guard::guard_invalid_recognize{} ] / action::effect_reject_recognize - , sml::state <= sml::state + , sml::state <= + sml::state + sml::event / action::effect_mark_uninitialized - , sml::state <= sml::state + , sml::state <= sml::state + sml::event / action::effect_mark_uninitialized @@ -199,6 +210,45 @@ struct model { , sml::state <= sml::state + sml::completion + //------------------------------------------------------------------------------// + // Pre-initialize recognize rejection (uninitialized origin). Mirrors the + // shared recognize error chain but terminates back in + // state_uninitialized so a rejected recognize can never promote the + // machine to ready without a successful initialize. + , sml::state <= + sml::state + + sml::completion [ guard::guard_has_recognize_error_out{} ] + / action::effect_store_recognize_error + , sml::state <= + sml::state + + sml::completion [ guard::guard_no_recognize_error_out{} ] + , sml::state <= + sml::state + + sml::completion [ guard::guard_has_recognize_error_callback{} ] + / action::effect_emit_recognize_error + , sml::state <= + sml::state + + sml::completion [ guard::guard_no_recognize_error_callback{} ] + + //------------------------------------------------------------------------------// + // Pre-initialize recognize rejection (errored origin). Same shape as the + // uninitialized chain; terminals return to state_errored so the failed + // initialize outcome stays observable and the pipeline stays unreachable. + , sml::state <= + sml::state + + sml::completion [ guard::guard_has_recognize_error_out{} ] + / action::effect_store_recognize_error + , sml::state <= + sml::state + + sml::completion [ guard::guard_no_recognize_error_out{} ] + , sml::state <= + sml::state + + sml::completion [ guard::guard_has_recognize_error_callback{} ] + / action::effect_emit_recognize_error + , sml::state <= + sml::state + + sml::completion [ guard::guard_no_recognize_error_callback{} ] + //------------------------------------------------------------------------------// // Unexpected events. , sml::state <= sml::state diff --git a/tests/speech/transcriber/lifecycle_tests.cpp b/tests/speech/transcriber/lifecycle_tests.cpp index f876353c..05218026 100644 --- a/tests/speech/transcriber/lifecycle_tests.cpp +++ b/tests/speech/transcriber/lifecycle_tests.cpp @@ -298,8 +298,9 @@ TEST_CASE("speech_transcriber_reports_backend_error_when_component_rejects") { } TEST_CASE("speech_transcriber_rejects_recognition_before_initialize") { - transcriber::sm engine{}; auto model = std::make_unique(); + const transcriber::dependencies deps = make_supported_dependencies(*model); + transcriber::sm engine{deps}; std::array pcm{}; std::array transcript{}; int32_t transcript_size = -1; @@ -333,6 +334,104 @@ TEST_CASE("speech_transcriber_rejects_recognition_before_initialize") { CHECK(err == emel::error::cast(transcriber::error::uninitialized)); CHECK(capture.calls == 1); CHECK(capture.err == emel::error::cast(transcriber::error::uninitialized)); + + // The rejection must return to state_uninitialized, not promote the machine + // to ready: a second pre-initialize recognize is rejected identically, so + // the pipeline stays unreachable without a successful initialize. + CHECK(engine.is(stateforward::sml::state)); + err = emel::error::cast(transcriber::error::none); + CHECK_FALSE(engine.process_event(recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::uninitialized)); + CHECK(capture.calls == 2); + CHECK(engine.is(stateforward::sml::state)); + + // A subsequent initialize still succeeds from uninitialized, after which + // recognize dispatches normally (the injected whisper-kind encoder rejects + // the synthetic model with a backend error and returns to ready). + err = emel::error::cast(transcriber::error::none); + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + REQUIRE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::none)); + CHECK(engine.is(stateforward::sml::state)); + + err = emel::error::cast(transcriber::error::none); + transcriber::event::recognize post_init_recognize_ev{ + *model, + make_supported_assets(), + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest}; + post_init_recognize_ev.error_out = &err; + CHECK_FALSE(engine.process_event(post_init_recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::backend)); + CHECK(engine.is(stateforward::sml::state)); +} + +TEST_CASE( + "speech_transcriber_recognition_after_failed_initialize_stays_errored") { + transcriber::sm engine{}; + auto model = std::make_unique(); + emel::error::type err = emel::error::cast(transcriber::error::none); + + // Drive the machine into state_errored via an invalid initialize. + transcriber::event::initialize invalid_initialize_ev{ + *model, transcriber::event::tokenizer_assets{}}; + invalid_initialize_ev.error_out = &err; + CHECK_FALSE(engine.process_event(invalid_initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::invalid_request)); + REQUIRE(engine.is(stateforward::sml::state)); + + std::array pcm{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + recognition_error_capture capture{}; + + transcriber::event::recognize recognize_ev{*model, + "{}", + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest}; + recognize_ev.error_out = &err; + recognize_ev.on_error = + emel::callback:: + from(&capture); + + // The rejection must return to state_errored, not promote the machine to + // ready: repeated recognize dispatches keep rejecting, so the pipeline is + // unreachable without a successful initialize. + err = emel::error::cast(transcriber::error::none); + CHECK_FALSE(engine.process_event(recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::uninitialized)); + CHECK(capture.calls == 1); + CHECK(capture.err == emel::error::cast(transcriber::error::uninitialized)); + CHECK(engine.is(stateforward::sml::state)); + + err = emel::error::cast(transcriber::error::none); + CHECK_FALSE(engine.process_event(recognize_ev)); + CHECK(err == emel::error::cast(transcriber::error::uninitialized)); + CHECK(capture.calls == 2); + CHECK(engine.is(stateforward::sml::state)); } TEST_CASE("speech_transcriber_returns_to_ready_after_rejected_recognition") { @@ -521,6 +620,68 @@ TEST_CASE("speech_transcriber_guards_validate_injected_dependencies") { zero_decoder_embedding.decoder_contract.embedding_length = 0; CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( *model, zero_decoder_embedding)); + + // The decode phase slices the encoder-state buffer as frame_count x decoder + // embedding_length, so contracts whose embedding lengths disagree are an + // unsupported dependency combination. + transcriber::dependencies mismatched_embedding = supported; + mismatched_embedding.decoder_contract.embedding_length = + supported.encoder_contract.embedding_length + 1; + CHECK_FALSE(transcriber::guard::guard_model_contracts_supported{}( + *model, mismatched_embedding)); +} + +TEST_CASE("speech_transcriber_encoder_success_requires_state_capacity") { + auto model = std::make_unique(); + transcriber::action::context ctx{}; + ctx.deps = make_supported_dependencies(*model); + + std::array pcm{}; + std::array encoder_state{}; + std::array transcript{}; + int32_t transcript_size = -1; + int32_t selected_token = -1; + float confidence = -1.0f; + int32_t encoder_frames = -1; + int32_t encoder_width = -1; + uint64_t encoder_digest = 1u; + uint64_t decoder_digest = 1u; + + transcriber::event::recognize recognize_ev{*model, + make_supported_assets(), + std::span{pcm}, + 16000, + std::span{transcript}, + transcript_size, + selected_token, + confidence, + encoder_frames, + encoder_width, + encoder_digest, + decoder_digest}; + recognize_ev.storage = transcriber::event::runtime_storage{ + .encoder_state = std::span{encoder_state}, + }; + transcriber::event::recognize_ctx recognize_ctx{}; + transcriber::event::recognize_run recognize_run{recognize_ev, recognize_ctx}; + + recognize_ctx.encoder_accepted = true; + recognize_ctx.err = emel::error::cast(transcriber::error::none); + + // Two frames of the 384-wide contract fit the 768-float buffer. + recognize_ctx.encoder_frame_count = 2; + CHECK(transcriber::guard::guard_encoder_success{}(recognize_run, ctx)); + CHECK_FALSE(transcriber::guard::guard_encoder_failure{}(recognize_run, ctx)); + + // Three frames would read past the caller's encoder-state span, so the + // encode outcome must route to the backend-error path. + recognize_ctx.encoder_frame_count = 3; + CHECK_FALSE(transcriber::guard::guard_encoder_success{}(recognize_run, ctx)); + CHECK(transcriber::guard::guard_encoder_failure{}(recognize_run, ctx)); + + // A negative frame count never counts as encoder success. + recognize_ctx.encoder_frame_count = -1; + CHECK_FALSE(transcriber::guard::guard_encoder_success{}(recognize_run, ctx)); } TEST_CASE("speech_transcriber_actions_store_and_emit_dispatch_outputs") { From 26c64a721093b415a41c4da99ec201106aa1ffeb Mon Sep 17 00:00:00 2001 From: gabewillen Date: Fri, 10 Jul 2026 23:39:29 -0500 Subject: [PATCH 5/5] fix: validate tokenizer assets at initialize and harden detokenize input Address three Codex P2 review findings: - Validate tokenizer JSON and decode policy before initialize succeeds. Add a component-level speech::tokenizer::event::validate asset-validation event and a validate flow in the whisper tokenizer machine (control-token JSON validation plus the same decode-policy support predicate the decoder uses). The transcriber's initialize flow now dispatches this event into the injected tokenizer actor from a new explicit state_tokenizer_validation_decision phase, so malformed tokenizer JSON or a default/unsupported decode policy fails initialization with tokenizer_invalid instead of running encode/decode work on the first recognize and surfacing a generic backend error. The transcriber stays model-family-neutral: validation semantics live in the tokenizer variant and are reached only through the injected component facade. - Reject negative token IDs before detokenizing. Direct detokenize requests containing negative IDs (including INT32_MIN) are rejected in the request guard with a distinct token_ids_invalid error before the decode action runs, and write_i32 now computes the digit magnitude in unsigned arithmetic so the minimum int32 renders without signed-negation overflow. Update the transcriber lifecycle fixtures to carry real whisper-tiny tokenizer JSON and the pinned decode policy as component-level pure data, and add tests for fail-fast initialize (malformed JSON, unsupported policy), negative-token-ID rejection, INT32_MIN rendering, and the validate flow through both the machine and the facade. --- .../mermaid/speech_tokenizer_whisper.mmd | 18 +++- .../mermaid/speech_transcriber.mmd | 4 +- .../architecture/speech_tokenizer_whisper.md | 36 ++++++- .planning/architecture/speech_transcriber.md | 8 +- src/emel/speech/tokenizer/any.hpp | 7 +- src/emel/speech/tokenizer/events.hpp | 14 +++ src/emel/speech/tokenizer/whisper/actions.hpp | 35 +++++++ src/emel/speech/tokenizer/whisper/detail.hpp | 15 +-- src/emel/speech/tokenizer/whisper/errors.hpp | 2 + src/emel/speech/tokenizer/whisper/events.hpp | 10 ++ src/emel/speech/tokenizer/whisper/guards.hpp | 78 +++++++++++++++ src/emel/speech/tokenizer/whisper/sm.hpp | 68 ++++++++++++- src/emel/speech/transcriber/actions.hpp | 18 ++++ src/emel/speech/transcriber/events.hpp | 1 + src/emel/speech/transcriber/guards.hpp | 19 ++++ src/emel/speech/transcriber/sm.hpp | 21 +++- tests/speech/tokenizer/whisper_tests.cpp | 86 ++++++++++++++++ tests/speech/transcriber/lifecycle_tests.cpp | 97 ++++++++++++++++++- 18 files changed, 517 insertions(+), 20 deletions(-) diff --git a/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd index b7ea33cc..524a0e6d 100644 --- a/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd +++ b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd @@ -2,8 +2,9 @@ stateDiagram-v2 direction TB [*] --> state_ready state_ready --> state_json_decision : detokenize_run [always] / effect_begin_detokenize_ - state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_tokenizer_json_valid_] / effect_detokenize_ + state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_detokenize_request_valid_] / effect_detokenize_ state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_tokenizer_json_invalid_] / effect_mark_tokenizer_json_invalid_ + state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_token_ids_invalid_] / effect_mark_token_ids_invalid_ state_detokenizing --> state_success_error_out_decision : completion_detokenize_run_ [always] / none state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none @@ -15,6 +16,16 @@ stateDiagram-v2 state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_no_error_callback_] / none state_done --> state_ready : completion_detokenize_run_ [always] / none state_errored --> state_ready : completion_detokenize_run_ [always] / none + state_ready --> state_validate_decision : validate_run [always] / effect_begin_validate_ + state_validate_decision --> state_validate_success_error_out_decision : completion_validate_run_ [guard_validate_supported_] / none + state_validate_decision --> state_validate_error_error_out_decision : completion_validate_run_ [guard_validate_json_invalid_] / effect_mark_validate_json_invalid_ + state_validate_decision --> state_validate_error_error_out_decision : completion_validate_run_ [guard_validate_policy_unsupported_] / effect_mark_validate_policy_unsupported_ + state_validate_success_error_out_decision --> state_validate_done : completion_validate_run_ [guard_validate_has_error_out_] / effect_store_validate_error_out_ + state_validate_success_error_out_decision --> state_validate_done : completion_validate_run_ [guard_validate_no_error_out_] / none + state_validate_error_error_out_decision --> state_validate_errored : completion_validate_run_ [guard_validate_has_error_out_] / effect_store_validate_error_out_ + state_validate_error_error_out_decision --> state_validate_errored : completion_validate_run_ [guard_validate_no_error_out_] / none + state_validate_done --> state_ready : completion_validate_run_ [always] / none + state_validate_errored --> state_ready : completion_validate_run_ [always] / none state_ready --> state_ready : _ [always] / effect_on_unexpected_ state_json_decision --> state_ready : _ [always] / effect_on_unexpected_ state_detokenizing --> state_ready : _ [always] / effect_on_unexpected_ @@ -24,3 +35,8 @@ stateDiagram-v2 state_error_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ state_done --> state_ready : _ [always] / effect_on_unexpected_ state_errored --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_success_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_error_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_done --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_errored --> state_ready : _ [always] / effect_on_unexpected_ diff --git a/.planning/architecture/mermaid/speech_transcriber.mmd b/.planning/architecture/mermaid/speech_transcriber.mmd index d5137287..64526921 100644 --- a/.planning/architecture/mermaid/speech_transcriber.mmd +++ b/.planning/architecture/mermaid/speech_transcriber.mmd @@ -7,8 +7,10 @@ stateDiagram-v2 state_ready --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ state_errored --> state_initialize_error_out_decision : initialize_run [always] / effect_reject_initialize_ state_initializing --> state_tokenizer_decision : completion_initialize_run_ [always] / none - state_tokenizer_decision --> state_model_support_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / none + state_tokenizer_decision --> state_tokenizer_validation_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / effect_validate_tokenizer_assets_ state_tokenizer_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_tokenizer_unsupported_] / effect_mark_tokenizer_invalid_ + state_tokenizer_validation_decision --> state_model_support_decision : completion_initialize_run_ [guard_tokenizer_validation_accepted_] / none + state_tokenizer_validation_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_tokenizer_validation_rejected_] / effect_mark_tokenizer_invalid_ state_model_support_decision --> state_initialize_success : completion_initialize_run_ [guard_initialize_model_supported_] / none state_model_support_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_unsupported_model_] / effect_mark_unsupported_model_ state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_success_ diff --git a/.planning/architecture/speech_tokenizer_whisper.md b/.planning/architecture/speech_tokenizer_whisper.md index 23c981a8..b444bddd 100644 --- a/.planning/architecture/speech_tokenizer_whisper.md +++ b/.planning/architecture/speech_tokenizer_whisper.md @@ -9,8 +9,9 @@ stateDiagram-v2 direction TB [*] --> state_ready state_ready --> state_json_decision : detokenize_run [always] / effect_begin_detokenize_ - state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_tokenizer_json_valid_] / effect_detokenize_ + state_json_decision --> state_detokenizing : completion_detokenize_run_ [guard_detokenize_request_valid_] / effect_detokenize_ state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_tokenizer_json_invalid_] / effect_mark_tokenizer_json_invalid_ + state_json_decision --> state_error_error_out_decision : completion_detokenize_run_ [guard_token_ids_invalid_] / effect_mark_token_ids_invalid_ state_detokenizing --> state_success_error_out_decision : completion_detokenize_run_ [always] / none state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_has_error_out_] / effect_store_error_out_ state_success_error_out_decision --> state_success_callback_decision : completion_detokenize_run_ [guard_no_error_out_] / none @@ -22,6 +23,16 @@ stateDiagram-v2 state_error_callback_decision --> state_errored : completion_detokenize_run_ [guard_no_error_callback_] / none state_done --> state_ready : completion_detokenize_run_ [always] / none state_errored --> state_ready : completion_detokenize_run_ [always] / none + state_ready --> state_validate_decision : validate_run [always] / effect_begin_validate_ + state_validate_decision --> state_validate_success_error_out_decision : completion_validate_run_ [guard_validate_supported_] / none + state_validate_decision --> state_validate_error_error_out_decision : completion_validate_run_ [guard_validate_json_invalid_] / effect_mark_validate_json_invalid_ + state_validate_decision --> state_validate_error_error_out_decision : completion_validate_run_ [guard_validate_policy_unsupported_] / effect_mark_validate_policy_unsupported_ + state_validate_success_error_out_decision --> state_validate_done : completion_validate_run_ [guard_validate_has_error_out_] / effect_store_validate_error_out_ + state_validate_success_error_out_decision --> state_validate_done : completion_validate_run_ [guard_validate_no_error_out_] / none + state_validate_error_error_out_decision --> state_validate_errored : completion_validate_run_ [guard_validate_has_error_out_] / effect_store_validate_error_out_ + state_validate_error_error_out_decision --> state_validate_errored : completion_validate_run_ [guard_validate_no_error_out_] / none + state_validate_done --> state_ready : completion_validate_run_ [always] / none + state_validate_errored --> state_ready : completion_validate_run_ [always] / none state_ready --> state_ready : _ [always] / effect_on_unexpected_ state_json_decision --> state_ready : _ [always] / effect_on_unexpected_ state_detokenizing --> state_ready : _ [always] / effect_on_unexpected_ @@ -31,6 +42,11 @@ stateDiagram-v2 state_error_callback_decision --> state_ready : _ [always] / effect_on_unexpected_ state_done --> state_ready : _ [always] / effect_on_unexpected_ state_errored --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_success_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_error_error_out_decision --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_done --> state_ready : _ [always] / effect_on_unexpected_ + state_validate_errored --> state_ready : _ [always] / effect_on_unexpected_ ``` ## Transitions @@ -38,8 +54,9 @@ stateDiagram-v2 | Source | Event | Guard | Action | Target | | --- | --- | --- | --- | --- | | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`detokenize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_begin_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | -| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_tokenizer_json_valid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_detokenize_request_valid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_detokenize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_tokenizer_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_mark_tokenizer_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_token_ids_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_mark_token_ids_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_has_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_store_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_success_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | @@ -51,6 +68,16 @@ stateDiagram-v2 | [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_no_error_callback>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`validate_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_begin_validate>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_mark_validate_json_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_policy_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_mark_validate_policy_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_has_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_store_validate_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_no_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_has_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_store_validate_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`guard_validate_no_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_validate_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_json_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_detokenizing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | @@ -60,3 +87,8 @@ stateDiagram-v2 | [`state_error_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_success_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_error_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_done`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | +| [`state_validate_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`_`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`effect_on_unexpected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/tokenizer/whisper/sm.hpp) | diff --git a/.planning/architecture/speech_transcriber.md b/.planning/architecture/speech_transcriber.md index a4d67291..ede3e3be 100644 --- a/.planning/architecture/speech_transcriber.md +++ b/.planning/architecture/speech_transcriber.md @@ -14,8 +14,10 @@ stateDiagram-v2 state_ready --> state_initialize_error_out_decision : initialize_run [guard_invalid_initialize_] / effect_reject_initialize_ state_errored --> state_initialize_error_out_decision : initialize_run [always] / effect_reject_initialize_ state_initializing --> state_tokenizer_decision : completion_initialize_run_ [always] / none - state_tokenizer_decision --> state_model_support_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / none + state_tokenizer_decision --> state_tokenizer_validation_decision : completion_initialize_run_ [guard_initialize_tokenizer_supported_] / effect_validate_tokenizer_assets_ state_tokenizer_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_tokenizer_unsupported_] / effect_mark_tokenizer_invalid_ + state_tokenizer_validation_decision --> state_model_support_decision : completion_initialize_run_ [guard_tokenizer_validation_accepted_] / none + state_tokenizer_validation_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_tokenizer_validation_rejected_] / effect_mark_tokenizer_invalid_ state_model_support_decision --> state_initialize_success : completion_initialize_run_ [guard_initialize_model_supported_] / none state_model_support_decision --> state_initialize_error_out_decision : completion_initialize_run_ [guard_initialize_unsupported_model_] / effect_mark_unsupported_model_ state_initialize_success --> state_initialize_done_callback_decision : completion_initialize_run_ [guard_has_initialize_error_out_] / effect_store_initialize_success_ @@ -73,8 +75,10 @@ stateDiagram-v2 | [`state_ready`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_invalid_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_errored`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`initialize_run`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_reject_initialize>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_initializing`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`always`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | -| [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_tokenizer_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_tokenizer_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_validate_tokenizer_assets>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_tokenizer_validation_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_tokenizer_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_tokenizer_unsupported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_tokenizer_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_tokenizer_validation_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_tokenizer_validation_accepted>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | +| [`state_tokenizer_validation_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_tokenizer_validation_rejected>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_tokenizer_invalid>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_model_supported>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`none`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_model_support_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_initialize_unsupported_model>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_mark_unsupported_model>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_error_out_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | | [`state_initialize_success`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`completion`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`guard_has_initialize_error_out>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`effect_store_initialize_success>`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | [`state_initialize_done_callback_decision`](https://github.com/stateforward/emel.cpp/blob/main/src/emel/speech/transcriber/sm.hpp) | diff --git a/src/emel/speech/tokenizer/any.hpp b/src/emel/speech/tokenizer/any.hpp index bbf02ead..f18e5c31 100644 --- a/src/emel/speech/tokenizer/any.hpp +++ b/src/emel/speech/tokenizer/any.hpp @@ -47,9 +47,14 @@ class any { return core_.process_event(ev); } + bool process_event(const event::validate &ev) { + return core_.process_event(ev); + } + private: using sm_list = stateforward::sml::aux::type_list; - using event_list = stateforward::sml::aux::type_list; + using event_list = + stateforward::sml::aux::type_list; emel::sm_any core_{}; }; diff --git a/src/emel/speech/tokenizer/events.hpp b/src/emel/speech/tokenizer/events.hpp index 2bb8095c..413238fd 100644 --- a/src/emel/speech/tokenizer/events.hpp +++ b/src/emel/speech/tokenizer/events.hpp @@ -76,6 +76,20 @@ struct detokenize { emel::callback on_error = {}; }; +// Asset-validation request: owners (e.g. the speech transcriber's initialize +// flow) dispatch this through the injected facade so the tokenizer variant can +// validate the tokenizer JSON and the bound decode policy before any pipeline +// work runs, instead of deferring the failure to the first detokenize. +struct validate { + validate(const std::string_view tokenizer_json_ref, + const asr_decode_policy &decode_policy_ref) noexcept + : tokenizer_json(tokenizer_json_ref), decode_policy(decode_policy_ref) {} + + std::string_view tokenizer_json = {}; + const asr_decode_policy &decode_policy; + emel::error::type *error_out = nullptr; +}; + } // namespace emel::speech::tokenizer::event namespace emel::speech::tokenizer::events { diff --git a/src/emel/speech/tokenizer/whisper/actions.hpp b/src/emel/speech/tokenizer/whisper/actions.hpp index 19441c74..a06ba5d5 100644 --- a/src/emel/speech/tokenizer/whisper/actions.hpp +++ b/src/emel/speech/tokenizer/whisper/actions.hpp @@ -43,6 +43,41 @@ struct effect_mark_tokenizer_json_invalid { } }; +struct effect_mark_token_ids_invalid { + void operator()(const event::detokenize_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::token_ids_invalid); + } +}; + +struct effect_begin_validate { + void operator()(const event::validate_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::none); + } +}; + +struct effect_mark_validate_json_invalid { + void operator()(const event::validate_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::tokenizer_json_invalid); + } +}; + +struct effect_mark_validate_policy_unsupported { + void operator()(const event::validate_run &runtime_ev, + context &) const noexcept { + runtime_ev.ctx.err = emel::error::cast(error::decode_policy_unsupported); + } +}; + +struct effect_store_validate_error_out { + void operator()(const event::validate_run &runtime_ev, + context &) const noexcept { + *runtime_ev.request.error_out = runtime_ev.ctx.err; + } +}; + struct effect_store_error_out { void operator()(const event::detokenize_run &runtime_ev, context &) const noexcept { diff --git a/src/emel/speech/tokenizer/whisper/detail.hpp b/src/emel/speech/tokenizer/whisper/detail.hpp index e0f4b785..6fcbbf2e 100644 --- a/src/emel/speech/tokenizer/whisper/detail.hpp +++ b/src/emel/speech/tokenizer/whisper/detail.hpp @@ -48,19 +48,22 @@ inline bool contains(std::string_view text, std::string_view needle) noexcept { inline uint32_t write_i32(const int32_t id, char *out) noexcept { char digits[12] = {}; - int32_t value = id; + // The magnitude is computed in unsigned arithmetic so the minimum int32 + // (whose negation does not fit in int32_t) renders correctly instead of + // hitting signed-negation overflow. + uint32_t magnitude = static_cast(id); uint32_t offset = 0; - if (value < 0) { + if (id < 0) { out[offset] = '-'; ++offset; - value = -value; + magnitude = 0u - magnitude; } uint32_t digits_count = 0; do { - digits[digits_count] = static_cast('0' + (value % 10)); - value /= 10; + digits[digits_count] = static_cast('0' + (magnitude % 10u)); + magnitude /= 10u; ++digits_count; - } while (value != 0); + } while (magnitude != 0u); while (digits_count > 0) { --digits_count; out[offset] = digits[digits_count]; diff --git a/src/emel/speech/tokenizer/whisper/errors.hpp b/src/emel/speech/tokenizer/whisper/errors.hpp index ec8cf537..b25fa532 100644 --- a/src/emel/speech/tokenizer/whisper/errors.hpp +++ b/src/emel/speech/tokenizer/whisper/errors.hpp @@ -8,6 +8,8 @@ enum class error : emel::error::type { none = 0, tokenizer_json_invalid = 1, internal_error = 2, + token_ids_invalid = 3, + decode_policy_unsupported = 4, }; } // namespace emel::speech::tokenizer::whisper diff --git a/src/emel/speech/tokenizer/whisper/events.hpp b/src/emel/speech/tokenizer/whisper/events.hpp index 86244b88..851ae03e 100644 --- a/src/emel/speech/tokenizer/whisper/events.hpp +++ b/src/emel/speech/tokenizer/whisper/events.hpp @@ -15,6 +15,7 @@ using detokenize_error = emel::speech::tokenizer::events::detokenize_error; namespace emel::speech::tokenizer::whisper::event { using detokenize = emel::speech::tokenizer::event::detokenize; +using validate = emel::speech::tokenizer::event::validate; struct detokenize_ctx { emel::error::type err = emel::error::cast(error::none); @@ -26,4 +27,13 @@ struct detokenize_run { detokenize_ctx &ctx; }; +struct validate_ctx { + emel::error::type err = emel::error::cast(error::none); +}; + +struct validate_run { + const validate &request; + validate_ctx &ctx; +}; + } // namespace emel::speech::tokenizer::whisper::event diff --git a/src/emel/speech/tokenizer/whisper/guards.hpp b/src/emel/speech/tokenizer/whisper/guards.hpp index b6b79811..d4424c70 100644 --- a/src/emel/speech/tokenizer/whisper/guards.hpp +++ b/src/emel/speech/tokenizer/whisper/guards.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include +#include "emel/speech/tokenizer/whisper/any.hpp" #include "emel/speech/tokenizer/whisper/detail.hpp" #include "emel/speech/tokenizer/whisper/events.hpp" @@ -33,6 +35,82 @@ struct guard_tokenizer_json_invalid { } }; +// Token IDs are vocab indices; negative values are malformed public input. The +// decode path renders unknown IDs through write_i32 for vocab lookup, and the +// minimum int32 needs dedicated magnitude handling there, so malformed IDs are +// rejected here before the decode action ever runs. The scan is a pure bounded +// read of the request span. +struct guard_token_ids_valid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + for (const int32_t token_id : runtime_ev.request.token_ids) { + if (token_id < 0) { + return false; + } + } + return true; + } +}; + +struct guard_detokenize_request_valid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return guard_tokenizer_json_valid{}(runtime_ev) && + guard_token_ids_valid{}(runtime_ev); + } +}; + +struct guard_token_ids_invalid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return guard_tokenizer_json_valid{}(runtime_ev) && + !guard_token_ids_valid{}(runtime_ev); + } +}; + +// Asset-validation guards: the validate flow checks the tokenizer JSON control +// tokens and the bound decode policy so owners can fail initialization instead +// of deferring the failure to the first detokenize after pipeline work ran. +struct guard_validate_json_valid { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return runtime_ev.request.tokenizer_json.data() != nullptr && + !runtime_ev.request.tokenizer_json.empty() && + detail::validate_tiny_control_tokens( + runtime_ev.request.tokenizer_json); + } +}; + +struct guard_validate_json_invalid { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return !guard_validate_json_valid{}(runtime_ev); + } +}; + +struct guard_validate_supported { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return guard_validate_json_valid{}(runtime_ev) && + is_tiny_asr_decode_policy_supported( + runtime_ev.request.decode_policy); + } +}; + +struct guard_validate_policy_unsupported { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return guard_validate_json_valid{}(runtime_ev) && + !is_tiny_asr_decode_policy_supported( + runtime_ev.request.decode_policy); + } +}; + +struct guard_validate_has_error_out { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return runtime_ev.request.error_out != nullptr; + } +}; + +struct guard_validate_no_error_out { + bool operator()(const event::validate_run &runtime_ev) const noexcept { + return runtime_ev.request.error_out == nullptr; + } +}; + struct guard_has_error_out { bool operator()(const event::detokenize_run &runtime_ev) const noexcept { return runtime_ev.request.error_out != nullptr; diff --git a/src/emel/speech/tokenizer/whisper/sm.hpp b/src/emel/speech/tokenizer/whisper/sm.hpp index cddd9d40..29914560 100644 --- a/src/emel/speech/tokenizer/whisper/sm.hpp +++ b/src/emel/speech/tokenizer/whisper/sm.hpp @@ -19,13 +19,21 @@ struct state_error_error_out_decision {}; struct state_error_callback_decision {}; struct state_done {}; struct state_errored {}; +struct state_validate_decision {}; +struct state_validate_success_error_out_decision {}; +struct state_validate_error_error_out_decision {}; +struct state_validate_done {}; +struct state_validate_errored {}; /* whisper detokenize machine state purpose - state_json_decision validates the request before any transcript bytes are - written: the tokenizer JSON must carry the tiny control tokens. + written: the tokenizer JSON must carry the tiny control tokens, the + caller-owned spans must be well formed, and every token ID must be + non-negative (vocab indices; negative IDs would reach the signed-magnitude + rendering in the decode path). - state_detokenizing performs the bounded token-id -> text decode in a single transition (data-plane iteration only; no behavior selection). The decode truncates to the caller's transcript capacity, matching the public @@ -35,6 +43,11 @@ state purpose transitions, never action-side branching. - state_done / state_errored return to state_ready by completion, so one machine instance serves repeated detokenize dispatches. +- the validate flow (state_validate_*) serves owner-driven asset validation: + it checks the tokenizer JSON control tokens and the bound decode policy so + owners (e.g. the transcriber's initialize phase) can fail fast instead of + deferring the failure to the first detokenize after pipeline work ran. The + flow publishes only through error_out and returns to state_ready. invariants - guards are pure predicates of the dispatch-run event; all context mutation @@ -53,11 +66,14 @@ struct model { + sml::event / action::effect_begin_detokenize{} , sml::state <= sml::state - + sml::completion [ guard::guard_tokenizer_json_valid{} ] + + sml::completion [ guard::guard_detokenize_request_valid{} ] / action::effect_detokenize{} , sml::state <= sml::state + sml::completion [ guard::guard_tokenizer_json_invalid{} ] / action::effect_mark_tokenizer_json_invalid{} + , sml::state <= sml::state + + sml::completion [ guard::guard_token_ids_invalid{} ] + / action::effect_mark_token_ids_invalid{} //------------------------------------------------------------------------------// // Publish. @@ -88,6 +104,37 @@ struct model { , sml::state <= sml::state + sml::completion + //------------------------------------------------------------------------------// + // Asset validation (owner-driven, error_out-only publication). + , sml::state <= sml::state + + sml::event + / action::effect_begin_validate{} + , sml::state <= + sml::state + + sml::completion [ guard::guard_validate_supported{} ] + , sml::state <= + sml::state + + sml::completion [ guard::guard_validate_json_invalid{} ] + / action::effect_mark_validate_json_invalid{} + , sml::state <= + sml::state + + sml::completion [ guard::guard_validate_policy_unsupported{} ] + / action::effect_mark_validate_policy_unsupported{} + , sml::state <= sml::state + + sml::completion [ guard::guard_validate_has_error_out{} ] + / action::effect_store_validate_error_out{} + , sml::state <= sml::state + + sml::completion [ guard::guard_validate_no_error_out{} ] + , sml::state <= sml::state + + sml::completion [ guard::guard_validate_has_error_out{} ] + / action::effect_store_validate_error_out{} + , sml::state <= sml::state + + sml::completion [ guard::guard_validate_no_error_out{} ] + , sml::state <= sml::state + + sml::completion + , sml::state <= sml::state + + sml::completion + //------------------------------------------------------------------------------// // Unexpected events. , sml::state <= sml::state + sml::unexpected_event @@ -108,6 +155,16 @@ struct model { + sml::unexpected_event / action::effect_on_unexpected{} , sml::state <= sml::state + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} + , sml::state <= sml::state + + sml::unexpected_event / action::effect_on_unexpected{} ); // clang-format on } @@ -126,6 +183,13 @@ struct sm : public emel::sm { const bool accepted = base_type::process_event(runtime_ev); return accepted && ctx.err == emel::error::cast(error::none); } + + bool process_event(const event::validate &ev) { + event::validate_ctx ctx{}; + event::validate_run runtime_ev{ev, ctx}; + const bool accepted = base_type::process_event(runtime_ev); + return accepted && ctx.err == emel::error::cast(error::none); + } }; } // namespace emel::speech::tokenizer::whisper diff --git a/src/emel/speech/transcriber/actions.hpp b/src/emel/speech/transcriber/actions.hpp index 60d3ce69..8a98261b 100644 --- a/src/emel/speech/transcriber/actions.hpp +++ b/src/emel/speech/transcriber/actions.hpp @@ -14,6 +14,22 @@ struct effect_begin_initialize { void operator()(const event::initialize_run &runtime_ev, context &) const noexcept { runtime_ev.ctx.err = detail::to_error(error::none); + runtime_ev.ctx.tokenizer_validation_accepted = false; + } +}; + +// Dispatches the component-level asset-validation event into the injected +// tokenizer actor so the variant validates the tokenizer JSON and the bound +// decode policy before initialization succeeds; whether initialization +// proceeds is decided by guards on the stored acceptance, never here. +struct effect_validate_tokenizer_assets { + void operator()(const event::initialize_run &runtime_ev, + context &ctx) const noexcept { + const speech::tokenizer::event::validate validate_ev{ + runtime_ev.request.tokenizer.model_json, ctx.deps.decode_policy}; + runtime_ev.ctx.tokenizer_validation_accepted = + ctx.tokenizer->process_event(validate_ev); + runtime_ev.ctx.err = detail::to_error(error::none); } }; @@ -240,6 +256,8 @@ struct effect_on_unexpected { }; inline constexpr effect_begin_initialize effect_begin_initialize{}; +inline constexpr effect_validate_tokenizer_assets + effect_validate_tokenizer_assets{}; inline constexpr effect_reject_initialize effect_reject_initialize{}; inline constexpr effect_mark_tokenizer_invalid effect_mark_tokenizer_invalid{}; inline constexpr effect_mark_unsupported_model effect_mark_unsupported_model{}; diff --git a/src/emel/speech/transcriber/events.hpp b/src/emel/speech/transcriber/events.hpp index 5dfe4d60..79780c4b 100644 --- a/src/emel/speech/transcriber/events.hpp +++ b/src/emel/speech/transcriber/events.hpp @@ -52,6 +52,7 @@ struct initialize { struct initialize_ctx { emel::error::type err = emel::error::cast(error::none); + bool tokenizer_validation_accepted = false; }; struct initialize_run { diff --git a/src/emel/speech/transcriber/guards.hpp b/src/emel/speech/transcriber/guards.hpp index 962ae35a..595e2907 100644 --- a/src/emel/speech/transcriber/guards.hpp +++ b/src/emel/speech/transcriber/guards.hpp @@ -82,6 +82,25 @@ struct guard_initialize_tokenizer_unsupported { } }; +// Outcome of the component-driven asset validation dispatched by +// effect_validate_tokenizer_assets: the injected tokenizer variant validated +// the tokenizer JSON and the bound decode policy, so initialization fails fast +// with tokenizer_invalid instead of deferring the failure to the first +// recognize after encode/decode work already ran. +struct guard_tokenizer_validation_accepted { + bool operator()(const event::initialize_run &runtime_ev, + const action::context &) const noexcept { + return runtime_ev.ctx.tokenizer_validation_accepted; + } +}; + +struct guard_tokenizer_validation_rejected { + bool operator()(const event::initialize_run &runtime_ev, + const action::context &ctx) const noexcept { + return !guard_tokenizer_validation_accepted{}(runtime_ev, ctx); + } +}; + struct guard_initialize_model_supported { bool operator()(const event::initialize_run &runtime_ev, const action::context &ctx) const noexcept { diff --git a/src/emel/speech/transcriber/sm.hpp b/src/emel/speech/transcriber/sm.hpp index f934242d..eb9bd7a3 100644 --- a/src/emel/speech/transcriber/sm.hpp +++ b/src/emel/speech/transcriber/sm.hpp @@ -14,6 +14,7 @@ namespace emel::speech::transcriber { struct state_uninitialized {}; struct state_initializing {}; struct state_tokenizer_decision {}; +struct state_tokenizer_validation_decision {}; struct state_model_support_decision {}; struct state_initialize_success {}; struct state_initialize_error_out_decision {}; @@ -43,8 +44,12 @@ speech transcriber engine (single source of truth) state purpose - initialize validates the injected dependencies against the request: the - tokenizer assets must match the pinned checksum and the component contracts - must have been bound against the same model the event carries. + tokenizer assets must match the pinned checksum, the component contracts + must have been bound against the same model the event carries, and the + injected tokenizer actor must accept the tokenizer JSON and the bound decode + policy (component-driven asset validation), so a transcriber never reaches + state_ready with assets or a decode policy the variant would reject at the + first detokenize after pipeline work already ran. - recognize drives the encode -> decode -> detokenize pipeline by dispatching into the injected component actors (speech/encoder, speech/decoder, speech/tokenizer facades); each phase outcome is an explicit decision state. @@ -92,13 +97,23 @@ struct model { , sml::state <= sml::state + sml::completion - , sml::state <= sml::state + , sml::state <= sml::state + sml::completion [ guard::guard_initialize_tokenizer_supported{} ] + / action::effect_validate_tokenizer_assets , sml::state <= sml::state + sml::completion [ guard::guard_initialize_tokenizer_unsupported{} ] / action::effect_mark_tokenizer_invalid + , sml::state <= + sml::state + + sml::completion + [ guard::guard_tokenizer_validation_accepted{} ] + , sml::state <= + sml::state + + sml::completion + [ guard::guard_tokenizer_validation_rejected{} ] + / action::effect_mark_tokenizer_invalid , sml::state <= sml::state + sml::completion [ guard::guard_initialize_model_supported{} ] diff --git a/tests/speech/tokenizer/whisper_tests.cpp b/tests/speech/tokenizer/whisper_tests.cpp index 0cb4a892..a3111526 100644 --- a/tests/speech/tokenizer/whisper_tests.cpp +++ b/tests/speech/tokenizer/whisper_tests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -136,6 +137,13 @@ TEST_CASE("speech whisper tokenizer rejects malformed token metadata") { const uint32_t negative_id_size = whisper::write_i32(-42, negative_id); CHECK(std::string_view{negative_id, negative_id_size} == "-42"); + // The minimum int32 has no in-range positive counterpart; the unsigned + // magnitude arithmetic must render it without signed-negation overflow. + char min_id[16] = {}; + const uint32_t min_id_size = + whisper::write_i32(std::numeric_limits::min(), min_id); + CHECK(std::string_view{min_id, min_id_size} == "-2147483648"); + CHECK_FALSE(whisper::validate_tiny_control_tokens("{}")); CHECK_FALSE(whisper::contains_token_role("{}", "<|endoftext|>", whisper::k_tiny_control_tokens.eot)); @@ -319,6 +327,84 @@ TEST_CASE("speech tokenizer facade preserves unsupported kinds") { CHECK(tokenizer::any::is_supported(cast_facade.kind())); } +TEST_CASE("speech whisper tokenizer rejects negative token ids") { + namespace tokenizer = emel::speech::tokenizer; + + const auto tokenizer_path = + repo_root() / "tests" / "models" / "tokenizer-tiny.json"; + REQUIRE(std::filesystem::exists(tokenizer_path)); + const std::string tokenizer_json = read_text_file(tokenizer_path); + + tokenizer::whisper::sm machine{}; + int32_t token_ids[] = {542, -1, std::numeric_limits::min()}; + char transcript[16] = {}; + int32_t transcript_size = -1; + emel::error::type err = emel::error::cast(tokenizer::whisper::error::none); + + tokenizer::event::detokenize request{ + tokenizer_json, std::span{token_ids}, + std::span{transcript}, transcript_size}; + request.error_out = &err; + CHECK_FALSE(machine.process_event(request)); + CHECK(err == emel::error::cast(tokenizer::whisper::error::token_ids_invalid)); + CHECK(transcript_size == 0); +} + +TEST_CASE("speech whisper tokenizer validates assets and decode policy") { + namespace tokenizer = emel::speech::tokenizer; + namespace whisper = emel::speech::tokenizer::whisper; + + const auto tokenizer_path = + repo_root() / "tests" / "models" / "tokenizer-tiny.json"; + REQUIRE(std::filesystem::exists(tokenizer_path)); + const std::string tokenizer_json = read_text_file(tokenizer_path); + + tokenizer::whisper::sm machine{}; + emel::error::type err = emel::error::cast(whisper::error::internal_error); + + // Valid JSON with the pinned decode policy passes. + tokenizer::event::validate good_request{tokenizer_json, + whisper::tiny_asr_decode_policy()}; + good_request.error_out = &err; + CHECK(machine.process_event(good_request)); + CHECK(err == emel::error::cast(whisper::error::none)); + + // Malformed JSON fails with tokenizer_json_invalid. + tokenizer::event::validate bad_json_request{ + "{}", whisper::tiny_asr_decode_policy()}; + bad_json_request.error_out = &err; + CHECK_FALSE(machine.process_event(bad_json_request)); + CHECK(err == emel::error::cast(whisper::error::tokenizer_json_invalid)); + + // A default (unsupported) decode policy fails with + // decode_policy_unsupported even when the JSON is valid. + const tokenizer::asr_decode_policy default_policy{}; + tokenizer::event::validate bad_policy_request{tokenizer_json, + default_policy}; + bad_policy_request.error_out = &err; + CHECK_FALSE(machine.process_event(bad_policy_request)); + CHECK(err == emel::error::cast(whisper::error::decode_policy_unsupported)); + + // The facade forwards validation the same way, and the machine still serves + // detokenize dispatches afterwards. + tokenizer::any facade{tokenizer::tokenizer_kind::whisper}; + tokenizer::event::validate facade_request{tokenizer_json, + whisper::tiny_asr_decode_policy()}; + CHECK(facade.process_event(facade_request)); + + int32_t token_ids[] = {542, 33, 898, 60}; + char transcript[16] = {}; + int32_t transcript_size = -1; + err = emel::error::cast(whisper::error::internal_error); + tokenizer::event::detokenize detokenize_request{ + tokenizer_json, std::span{token_ids}, + std::span{transcript}, transcript_size}; + detokenize_request.error_out = &err; + CHECK(machine.process_event(detokenize_request)); + CHECK(err == emel::error::cast(whisper::error::none)); + CHECK(transcript_size > 0); +} + TEST_CASE("speech whisper tokenizer clamps compacted output to capacity") { namespace whisper = emel::speech::tokenizer::whisper::detail; diff --git a/tests/speech/transcriber/lifecycle_tests.cpp b/tests/speech/transcriber/lifecycle_tests.cpp index 05218026..bd5f0ae5 100644 --- a/tests/speech/transcriber/lifecycle_tests.cpp +++ b/tests/speech/transcriber/lifecycle_tests.cpp @@ -2,8 +2,12 @@ #include #include +#include +#include +#include #include #include +#include #include #include "emel/error/error.hpp" @@ -16,6 +20,46 @@ namespace transcriber = emel::speech::transcriber; inline constexpr std::string_view k_backend_sha = "test-backend-sha"; +std::filesystem::path repo_root() { +#ifdef EMEL_TEST_REPO_ROOT + return std::filesystem::path{EMEL_TEST_REPO_ROOT}; +#else + return std::filesystem::current_path(); +#endif +} + +// Real whisper-tiny tokenizer JSON: the initialize flow now drives +// component-level asset validation through the injected tokenizer actor, so +// supported fixtures must carry JSON the tokenizer variant accepts. +const std::string &tiny_tokenizer_json() { + static const std::string json = [] { + std::ifstream stream(repo_root() / "tests" / "models" / + "tokenizer-tiny.json", + std::ios::binary); + return std::string{std::istreambuf_iterator{stream}, + std::istreambuf_iterator{}}; + }(); + return json; +} + +// Component-level decode policy carrying the pinned whisper-tiny control-token +// ids as pure data (no variant headers or namespaces). +emel::speech::tokenizer::asr_decode_policy make_supported_policy() noexcept { + emel::speech::tokenizer::asr_decode_policy policy{}; + policy.tokens.eot = 50257; + policy.tokens.sot = 50258; + policy.tokens.language_en = 50259; + policy.tokens.translate = 50358; + policy.tokens.transcribe = 50359; + policy.tokens.no_speech = 50362; + policy.tokens.notimestamps = 50363; + policy.tokens.timestamp_begin = 50364; + policy.tokens.space = 220; + policy.suppress_translate = true; + policy.prompt_tokens = {50258, 50259, 50359}; + return policy; +} + struct initialize_error_capture { int32_t calls = 0; emel::error::type err = emel::error::cast(transcriber::error::none); @@ -104,14 +148,14 @@ make_supported_dependencies(const emel::model::data &model) noexcept { .embedding_length = 384, .decoder_block_count = 4, }; - deps.decode_policy = emel::speech::tokenizer::asr_decode_policy{}; + deps.decode_policy = make_supported_policy(); deps.tokenizer_sha256 = k_backend_sha; return deps; } transcriber::event::tokenizer_assets make_supported_assets() noexcept { return transcriber::event::tokenizer_assets{ - .model_json = "{}", + .model_json = tiny_tokenizer_json(), .sha256 = k_backend_sha, }; } @@ -156,11 +200,60 @@ TEST_CASE("speech_transcriber_rejects_unsupported_tokenizer") { emel::error::cast(transcriber::error::tokenizer_invalid)); } +TEST_CASE("speech_transcriber_rejects_invalid_tokenizer_json_at_initialize") { + auto model = std::make_unique(); + const transcriber::dependencies deps = make_supported_dependencies(*model); + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + initialize_error_capture capture{}; + + // The checksum matches the injected dependency pin, but the JSON itself is + // malformed (missing the control tokens); the component-driven validation + // must fail initialization instead of deferring the failure to the first + // detokenize after pipeline work ran. + const transcriber::event::tokenizer_assets malformed_assets{ + .model_json = "{}", + .sha256 = k_backend_sha, + }; + transcriber::event::initialize initialize_ev{*model, malformed_assets}; + initialize_ev.error_out = &err; + initialize_ev.on_error = + emel::callback::from< + initialize_error_capture, record_initialize_error>(&capture); + + CHECK_FALSE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::tokenizer_invalid)); + CHECK(capture.calls == 1); + CHECK(capture.err == + emel::error::cast(transcriber::error::tokenizer_invalid)); + CHECK(engine.is(stateforward::sml::state)); +} + +TEST_CASE( + "speech_transcriber_rejects_unsupported_decode_policy_at_initialize") { + auto model = std::make_unique(); + transcriber::dependencies deps = make_supported_dependencies(*model); + deps.decode_policy = emel::speech::tokenizer::asr_decode_policy{}; + transcriber::sm engine{deps}; + emel::error::type err = emel::error::cast(transcriber::error::none); + + // The decode policy is part of the injected dependency contract; a default + // (unsupported) policy must fail initialization instead of running the + // encoder on the first recognize and failing in the decoder. + transcriber::event::initialize initialize_ev{*model, make_supported_assets()}; + initialize_ev.error_out = &err; + + CHECK_FALSE(engine.process_event(initialize_ev)); + CHECK(err == emel::error::cast(transcriber::error::tokenizer_invalid)); + CHECK(engine.is(stateforward::sml::state)); +} + TEST_CASE("speech_transcriber_rejects_unsupported_initialized_model") { auto model = std::make_unique(); transcriber::dependencies deps{}; deps.tokenizer_kind = emel::speech::tokenizer::tokenizer_kind::whisper; deps.tokenizer_sha256 = k_backend_sha; + deps.decode_policy = make_supported_policy(); transcriber::sm engine{deps}; emel::error::type err = emel::error::cast(transcriber::error::none);