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/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd new file mode 100644 index 00000000..524a0e6d --- /dev/null +++ b/.planning/architecture/mermaid/speech_tokenizer_whisper.mmd @@ -0,0 +1,42 @@ +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_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 + 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_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_ + 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_ + 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 new file mode 100644 index 00000000..64526921 --- /dev/null +++ b/.planning/architecture/mermaid/speech_transcriber.mmd @@ -0,0 +1,58 @@ +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_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_ + 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_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 + 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_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/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..b444bddd --- /dev/null +++ b/.planning/architecture/speech_tokenizer_whisper.md @@ -0,0 +1,94 @@ +# 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_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 + 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_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_ + 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_ + 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 + +| 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_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) | +| [`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) | [`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) | +| [`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) | +| [`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 new file mode 100644 index 00000000..ede3e3be --- /dev/null +++ b/.planning/architecture/speech_transcriber.md @@ -0,0 +1,126 @@ +# 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_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_ + 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_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 + 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_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_ +``` + +## 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) | [`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) | +| [`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_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) | +| [`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_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/.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/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/sm.hpp b/src/emel/sm.hpp index e0745c1b..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,12 +1044,26 @@ 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 + // (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/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..69196117 --- /dev/null +++ b/src/emel/speech/decoder/any.hpp @@ -0,0 +1,57 @@ +#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 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, +}; + +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(); } + + // 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); + } + +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..1fe67352 --- /dev/null +++ b/src/emel/speech/encoder/any.hpp @@ -0,0 +1,57 @@ +#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 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, +}; + +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(); } + + // 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); + } + +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..f18e5c31 --- /dev/null +++ b/src/emel/speech/tokenizer/any.hpp @@ -0,0 +1,62 @@ +#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 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, +}; + +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(); } + + // 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); + } + + 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; + + 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..413238fd --- /dev/null +++ b/src/emel/speech/tokenizer/events.hpp @@ -0,0 +1,107 @@ +#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 = {}; +}; + +// 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 { + +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..a06ba5d5 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/actions.hpp @@ -0,0 +1,113 @@ +#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 {}; + +// 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 &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 { + 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_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 { + *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..6fcbbf2e 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{ @@ -63,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]; @@ -107,16 +95,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 +155,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 +234,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 +255,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 +321,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..b25fa532 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/errors.hpp @@ -0,0 +1,15 @@ +#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, + 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 new file mode 100644 index 00000000..851ae03e --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/events.hpp @@ -0,0 +1,39 @@ +#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; +using validate = emel::speech::tokenizer::event::validate; + +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; +}; + +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 new file mode 100644 index 00000000..d4424c70 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/guards.hpp @@ -0,0 +1,150 @@ +#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" + +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); + } +}; + +struct guard_tokenizer_json_invalid { + bool operator()(const event::detokenize_run &runtime_ev) const noexcept { + return !guard_tokenizer_json_valid{}(runtime_ev); + } +}; + +// 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; + } +}; + +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..29914560 --- /dev/null +++ b/src/emel/speech/tokenizer/whisper/sm.hpp @@ -0,0 +1,195 @@ +#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 {}; +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, 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 + 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. +- 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 + 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_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. + , 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 + + //------------------------------------------------------------------------------// + // 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 + / 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{} + , 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); + } + + 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/recognizer/actions.hpp b/src/emel/speech/transcriber/actions.hpp similarity index 66% rename from src/emel/speech/recognizer/actions.hpp rename to src/emel/speech/transcriber/actions.hpp index afa50663..8a98261b 100644 --- a/src/emel/speech/recognizer/actions.hpp +++ b/src/emel/speech/transcriber/actions.hpp @@ -1,15 +1,35 @@ #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, 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); } }; @@ -76,6 +96,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 +133,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 { @@ -173,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{}; @@ -184,6 +269,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 +284,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..79780c4b 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 = {}; @@ -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 { @@ -169,6 +170,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 +186,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 +216,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 50% rename from src/emel/speech/recognizer/guards.hpp rename to src/emel/speech/transcriber/guards.hpp index 3bf92bc2..595e2907 100644 --- a/src/emel/speech/recognizer/guards.hpp +++ b/src/emel/speech/transcriber/guards.hpp @@ -1,10 +1,12 @@ #pragma once -#include "emel/speech/recognizer/context.hpp" -#include "emel/speech/recognizer/detail.hpp" -#include "emel/speech/recognizer/events.hpp" +#include -namespace emel::speech::recognizer::guard { +#include "emel/speech/transcriber/context.hpp" +#include "emel/speech/transcriber/detail.hpp" +#include "emel/speech/transcriber/events.hpp" + +namespace emel::speech::transcriber::guard { struct guard_valid_initialize { bool operator()(const event::initialize_run &runtime_ev, @@ -21,42 +23,96 @@ struct guard_invalid_initialize { } }; -template struct guard_initialize_tokenizer_supported { +// 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. +// +// 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 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; + } +}; + +// 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 { + 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 && + deps.decoder_contract.vocab_size > 0 && + deps.decoder_contract.embedding_length > 0 && + deps.encoder_contract.embedding_length == + deps.decoder_contract.embedding_length; + } +}; + +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); + const action::context &ctx) const noexcept { + return guard_tokenizer_assets_supported{}(runtime_ev.request.tokenizer, + ctx.deps); } }; -template struct guard_initialize_tokenizer_unsupported { +struct guard_initialize_tokenizer_unsupported { bool operator()(const event::initialize_run &runtime_ev, const action::context &ctx) const noexcept { - return !guard_initialize_tokenizer_supported{}(runtime_ev, - ctx); + return !guard_initialize_tokenizer_supported{}(runtime_ev, ctx); } }; -template struct guard_initialize_model_supported { +// 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 typename route_policy::guard_model_supported{}( - runtime_ev.request.model); + 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); } }; -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,25 +175,38 @@ 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); } }; +// 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(); } }; @@ -163,6 +232,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 +289,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 51% rename from src/emel/speech/recognizer/sm.hpp rename to src/emel/speech/transcriber/sm.hpp index 1e76b429..eb9bd7a3 100644 --- a/src/emel/speech/recognizer/sm.hpp +++ b/src/emel/speech/transcriber/sm.hpp @@ -4,50 +4,74 @@ #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_tokenizer_validation_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 {}; 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 {}; -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, 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. +- 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. -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 +97,29 @@ 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{} ] + / action::effect_validate_tokenizer_assets , 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_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_unsupported_model{} ] + [ guard::guard_initialize_model_supported{} ] + , sml::state <= sml::state + + sml::completion + [ guard::guard_initialize_unsupported_model{} ] / action::effect_mark_unsupported_model , sml::state <= sml::state @@ -113,49 +147,56 @@ 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 + 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 , 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 @@ -184,6 +225,45 @@ template 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 @@ -197,13 +277,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 +311,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/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/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/tokenizer/whisper_tests.cpp b/tests/speech/tokenizer/whisper_tests.cpp index 1254d5c8..a3111526 100644 --- a/tests/speech/tokenizer/whisper_tests.cpp +++ b/tests/speech/tokenizer/whisper_tests.cpp @@ -3,11 +3,17 @@ #include #include #include +#include #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 { @@ -131,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)); @@ -208,6 +221,190 @@ 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 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 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 new file mode 100644 index 00000000..bd5f0ae5 --- /dev/null +++ b/tests/speech/transcriber/lifecycle_tests.cpp @@ -0,0 +1,927 @@ +#include "doctest/doctest.h" + +#include +#include +#include +#include +#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"; + +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); +}; + +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 = 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 = tiny_tokenizer_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_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); + + 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") { + 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; + 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)); + + // 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") { + 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)); + + // 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", + }; + 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)); + + // 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)); + + 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)); + + // 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") { + 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,