chantier B STT #1 : S1 unification engine (mel partagé + skeleton stt + VAD C++)

S1.1 mel extractor unifié (kazeia_mel.{h,cpp}) :
 - Paramétrable : window (sym/periodic), compression (log/log10), post-norm
   (none/whisper), FFT auto radix-2 (zero-pad N_FFT pas puissance de 2)
 - Configs pré-faites : config_qwen3_tts_speaker() + config_whisper()
 - speaker_encoder.cpp migré dessus : régression damien cos=0.9997 et bit-exact
   (max abs diff = 0) vs avant. E2E clonage in-process intact (RTF inchangé).

S1.2 audit ORT C++ :
 - libonnxruntime.so + headers C++ trouvés dans cache Gradle (AAR Maven
   onnxruntime-android-qnn:1.24.3). API_VERSION=24, QNN EP intégré.
 - headers/ copiés dans dist/include/onnxruntime/ (versionné, ~1 MB)
 - libonnxruntime.so copiée dans dist/lib/ (gitignore, 20 MB, README pour
   reproduire l'extraction).

S1.3 skeleton stt_engine :
 - stt_engine.{h,cpp} : API parallèle à tts_engine.h
   (SttEngineLoadCfg / SttTranscribeCfg / SttTranscribeResult)
 - VAD RMS C++ complet (port de VadStage.kt prod : frame=1600, seuil=150,
   3 frames speech, 8 frames silence)
 - stt_engine_load + stt_engine_transcribe : stubs err=-99 jusqu'au port ORT
   QNN en S2 (encoder NPU + decoder loop KV-cache + tokenizer BPE)
 - Binaire kazeia_stt_test (1.2 MB statique) valide mel Whisper [80,3000] +
   VAD sur audio FR 5s, plage de valeurs cohérente.

Reste S2 : port encoder/decoder NPU via Ort::Session + QNN EP, port tokenizer
BPE byte-level, override transcribe. Quand S2 done : S3 = JNI + façade Kotlin
+ suppression libmel_extractor.so / VadStage.kt / WhisperHybridEngine.kt.
This commit is contained in:
Richard Loyer 2026-05-31 19:19:28 +02:00
parent 82d206137f
commit 4b43d9e3d7
23 changed files with 20768 additions and 104 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ eval/out/
dist/b-vulkan/ dist/b-vulkan/
dist/lib-chraac/ dist/lib-chraac/
models/ models/
dist/lib/libonnxruntime.so

View File

@ -21,6 +21,7 @@ CFLAGS=(
INCS=( -I"$INC" ) INCS=( -I"$INC" )
LIBS=( LIBS=(
"$JNI/speaker_encoder.cpp" "$JNI/speaker_encoder.cpp"
"$JNI/kazeia_mel.cpp"
-L"$LIB" -lggml -lggml-base -lggml-cpu -L"$LIB" -lggml -lggml-base -lggml-cpu
-Wl,-rpath,'$ORIGIN' -Wl,-rpath,'$ORIGIN'
-llog -ldl -lm -llog -ldl -lm

28
dist/build_stt_test.sh vendored Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Build kazeia_stt_test : binaire smoke-test S1.3 (mel Whisper + VAD RMS).
# Pas encore de dépendance ORT/QNN (ça vient en S2).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
NDK="${ANDROID_NDK_ROOT:-/opt/Kazeia/android-ndk-r27d}"
CXX="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang++"
JNI="$HERE/jni"
OUT="$HERE/b-jni/kazeia_stt_test"
mkdir -p "$HERE/b-jni"
CFLAGS=(
-std=c++17 -O3 -fPIE
-march=armv8.6-a+i8mm+bf16+dotprod+fp16
-Wno-unused-parameter -Wno-unused-variable -Wno-sign-compare
-static-libstdc++
)
LIBS=(
"$JNI/stt_test.cpp"
"$JNI/stt_engine.cpp"
"$JNI/kazeia_mel.cpp"
-llog -ldl -lm
)
echo "== building $OUT =="
"$CXX" "${CFLAGS[@]}" "${LIBS[@]}" -o "$OUT"
ls -la "$OUT"

View File

@ -37,6 +37,7 @@ LIBS=(
"$JNI/sampler.cpp" "$JNI/sampler.cpp"
"$JNI/kazeia_text_tokenizer.cpp" "$JNI/kazeia_text_tokenizer.cpp"
"$JNI/speaker_encoder.cpp" "$JNI/speaker_encoder.cpp"
"$JNI/kazeia_mel.cpp"
"$DECODER_A" "$DECODER_A"
-L"$LIB" -lllama -lggml -lggml-base -lggml-cpu -L"$LIB" -lllama -lggml -lggml-base -lggml-cpu
-Wl,-rpath,'$ORIGIN' -Wl,-rpath,'$ORIGIN'

View File

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "onnxruntime_c_api.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* \param use_arena zero: false. non-zero: true.
*/
ORT_EXPORT
ORT_API_STATUS(OrtSessionOptionsAppendExecutionProvider_CPU, _In_ OrtSessionOptions* options, int use_arena)
ORT_ALL_ARGS_NONNULL;
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
// This file contains well-known keys for OrtEnv configuration entries, which may be used to configure EPs or
// other global settings.
// Refer to OrtEnvCreationOptions::config_entries and OrtApi::CreateEnvWithOptions.
// This file does NOT specify all available keys as EPs may accept custom entries with the prefix "ep.<ep_name>.".
// Key for a boolean option that, when enabled, allows EP factories to create virtual OrtHardwareDevice
// instances via OrtEpApi::CreateHardwareDevice().
//
// This config entry is automatically set to "1" by ORT if an application registers an EP library with a registration
// name that ends in the suffix ".virtual". See OrtApi::RegisterExecutionProviderLibrary().
//
// Note: A virtual OrtHardwareDevice does not represent actual hardware on the device, and is identified via the
// metadata entry "is_virtual" with a value of "1".
//
// Allowed values:
// - "0": Default. Creation of virtual devices is not allowed.
// This is the assumed default value if this key is not present in the environment's configuration entries.
// - "1": Creation of virtual devices is allowed.
static const char* const kOrtEnvAllowVirtualDevices = "allow_virtual_devices";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
// This file contains well-known keys for OrtEpDevice and OrtHardwareDevice metadata entries.
// It does NOT specify all available metadata keys.
// Key for the execution provider version string. This should be available for all plugin EPs.
static const char* const kOrtEpDevice_EpMetadataKey_Version = "version";
// Key for the execution provider OS driver version.
static const char* const kOrtEpDevice_EpMetadataKey_OSDriverVersion = "os_driver_version";
// Prefix for execution provider compatibility information stored in model metadata.
// Used when generating EP context models to store compatibility strings for each EP.
// Full key format: "ep_compatibility_info.<EP_TYPE>"
static const char* const kOrtModelMetadata_EpCompatibilityInfoPrefix = "ep_compatibility_info.";
// Key for the execution provider library path (for dynamically loaded EPs)
static const char* const kOrtEpDevice_EpMetadataKey_LibraryPath = "library_path";
// Optional metadata key to determine if a OrtHardwareDevice represents a virtual (non-hardware) device.
// Possible values:
// - "0": OrtHardwareDevice is not virtual (i.e., actual hardware device). This is the assumed default value
// if this metadata key is not present.
// - "1": OrtHardwareDevice is virtual.
static const char* const kOrtHardwareDevice_MetadataKey_IsVirtual = "is_virtual";

View File

@ -0,0 +1,537 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include <stdint.h>
#include <cmath>
#include <cstring>
#include <limits>
namespace onnxruntime_float16 {
namespace detail {
enum class endian {
#if defined(_WIN32)
little = 0,
big = 1,
native = little,
#elif defined(__GNUC__) || defined(__clang__)
little = __ORDER_LITTLE_ENDIAN__,
big = __ORDER_BIG_ENDIAN__,
native = __BYTE_ORDER__,
#else
#error onnxruntime_float16::detail::endian is not implemented in this environment.
#endif
};
static_assert(
endian::native == endian::little || endian::native == endian::big,
"Only little-endian or big-endian native byte orders are supported.");
} // namespace detail
/// <summary>
/// Shared implementation between public and internal classes. CRTP pattern.
/// </summary>
template <class Derived>
struct Float16Impl {
protected:
/// <summary>
/// Converts from float to uint16_t float16 representation
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
constexpr static uint16_t ToUint16Impl(float v) noexcept;
/// <summary>
/// Converts float16 to float
/// </summary>
/// <returns>float representation of float16 value</returns>
float ToFloatImpl() const noexcept;
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
uint16_t AbsImpl() const noexcept {
return static_cast<uint16_t>(val & ~kSignMask);
}
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
uint16_t NegateImpl() const noexcept {
return IsNaN() ? val : static_cast<uint16_t>(val ^ kSignMask);
}
public:
// uint16_t special values
static constexpr uint16_t kSignMask = 0x8000U;
static constexpr uint16_t kBiasedExponentMask = 0x7C00U;
static constexpr uint16_t kPositiveInfinityBits = 0x7C00U;
static constexpr uint16_t kNegativeInfinityBits = 0xFC00U;
static constexpr uint16_t kPositiveQNaNBits = 0x7E00U;
static constexpr uint16_t kNegativeQNaNBits = 0xFE00U;
static constexpr uint16_t kMaxValueBits = 0x7BFFU; // Largest normal number
static constexpr uint16_t kMinValueBits = 0xFBFFU; // Lowest normal number
static constexpr uint16_t kOneBits = 0x3C00U;
static constexpr uint16_t kMinusOneBits = 0xBC00U;
uint16_t val{0};
Float16Impl() = default;
/// <summary>
/// Checks if the value is negative
/// </summary>
/// <returns>true if negative</returns>
bool IsNegative() const noexcept {
return static_cast<int16_t>(val) < 0;
}
/// <summary>
/// Tests if the value is NaN
/// </summary>
/// <returns>true if NaN</returns>
bool IsNaN() const noexcept {
return AbsImpl() > kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is finite
/// </summary>
/// <returns>true if finite</returns>
bool IsFinite() const noexcept {
return AbsImpl() < kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents positive infinity.
/// </summary>
/// <returns>true if positive infinity</returns>
bool IsPositiveInfinity() const noexcept {
return val == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents negative infinity
/// </summary>
/// <returns>true if negative infinity</returns>
bool IsNegativeInfinity() const noexcept {
return val == kNegativeInfinityBits;
}
/// <summary>
/// Tests if the value is either positive or negative infinity.
/// </summary>
/// <returns>True if absolute value is infinity</returns>
bool IsInfinity() const noexcept {
return AbsImpl() == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is NaN or zero. Useful for comparisons.
/// </summary>
/// <returns>True if NaN or zero.</returns>
bool IsNaNOrZero() const noexcept {
auto abs = AbsImpl();
return (abs == 0 || abs > kPositiveInfinityBits);
}
/// <summary>
/// Tests if the value is normal (not zero, subnormal, infinite, or NaN).
/// </summary>
/// <returns>True if so</returns>
bool IsNormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) != 0); // is not subnormal (has a non-zero exponent)
}
/// <summary>
/// Tests if the value is subnormal (denormal).
/// </summary>
/// <returns>True if so</returns>
bool IsSubnormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) == 0); // is subnormal (has a zero exponent)
}
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
Derived Abs() const noexcept { return Derived::FromBits(AbsImpl()); }
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
Derived Negate() const noexcept { return Derived::FromBits(NegateImpl()); }
/// <summary>
/// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
/// for two values by or'ing the private bits together and stripping the sign. They are both zero,
/// and therefore equivalent, if the resulting value is still zero.
/// </summary>
/// <param name="lhs">first value</param>
/// <param name="rhs">second value</param>
/// <returns>True if both arguments represent zero</returns>
static bool AreZero(const Float16Impl& lhs, const Float16Impl& rhs) noexcept {
return static_cast<uint16_t>((lhs.val | rhs.val) & ~kSignMask) == 0;
}
bool operator==(const Float16Impl& rhs) const noexcept {
if (IsNaN() || rhs.IsNaN()) {
// IEEE defines that NaN is not equal to anything, including itself.
return false;
}
return val == rhs.val;
}
bool operator!=(const Float16Impl& rhs) const noexcept { return !(*this == rhs); }
bool operator<(const Float16Impl& rhs) const noexcept {
if (IsNaN() || rhs.IsNaN()) {
// IEEE defines that NaN is unordered with respect to everything, including itself.
return false;
}
const bool left_is_negative = IsNegative();
if (left_is_negative != rhs.IsNegative()) {
// When the signs of left and right differ, we know that left is less than right if it is
// the negative value. The exception to this is if both values are zero, in which case IEEE
// says they should be equal, even if the signs differ.
return left_is_negative && !AreZero(*this, rhs);
}
return (val != rhs.val) && ((val < rhs.val) ^ left_is_negative);
}
};
// The following Float16_t conversions are based on the code from
// Eigen library.
// The conversion routines are Copyright (c) Fabian Giesen, 2016.
// The original license follows:
//
// Copyright (c) Fabian Giesen, 2016
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
namespace detail {
union float32_bits {
unsigned int u;
float f;
};
} // namespace detail
template <class Derived>
inline constexpr uint16_t Float16Impl<Derived>::ToUint16Impl(float v) noexcept {
detail::float32_bits f{};
f.f = v;
constexpr detail::float32_bits f32infty = {255 << 23};
constexpr detail::float32_bits f16max = {(127 + 16) << 23};
constexpr detail::float32_bits denorm_magic = {((127 - 15) + (23 - 10) + 1) << 23};
constexpr unsigned int sign_mask = 0x80000000u;
uint16_t val = static_cast<uint16_t>(0x0u);
unsigned int sign = f.u & sign_mask;
f.u ^= sign;
// NOTE all the integer compares in this function can be safely
// compiled into signed compares since all operands are below
// 0x80000000. Important if you want fast straight SSE2 code
// (since there's no unsigned PCMPGTD).
if (f.u >= f16max.u) { // result is Inf or NaN (all exponent bits set)
val = (f.u > f32infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf
} else { // (De)normalized number or zero
if (f.u < (113 << 23)) { // resulting FP16 is subnormal or zero
// use a magic value to align our 10 mantissa bits at the bottom of
// the float. as long as FP addition is round-to-nearest-even this
// just works.
f.f += denorm_magic.f;
// and one integer subtract of the bias later, we have our final float!
val = static_cast<uint16_t>(f.u - denorm_magic.u);
} else {
unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd
// update exponent, rounding bias part 1
// Equivalent to `f.u += ((unsigned int)(15 - 127) << 23) + 0xfff`, but
// without arithmetic overflow.
f.u += 0xc8000fffU;
// rounding bias part 2
f.u += mant_odd;
// take the bits!
val = static_cast<uint16_t>(f.u >> 13);
}
}
val |= static_cast<uint16_t>(sign >> 16);
return val;
}
template <class Derived>
inline float Float16Impl<Derived>::ToFloatImpl() const noexcept {
constexpr detail::float32_bits magic = {113 << 23};
constexpr unsigned int shifted_exp = 0x7c00 << 13; // exponent mask after shift
detail::float32_bits o{};
o.u = (val & 0x7fff) << 13; // exponent/mantissa bits
unsigned int exp = shifted_exp & o.u; // just the exponent
o.u += (127 - 15) << 23; // exponent adjust
// handle exponent special cases
if (exp == shifted_exp) { // Inf/NaN?
o.u += (128 - 16) << 23; // extra exp adjust
} else if (exp == 0) { // Zero/Denormal?
o.u += 1 << 23; // extra exp adjust
o.f -= magic.f; // re-normalize
}
// Attempt to workaround the Internal Compiler Error on ARM64
// for bitwise | operator, including std::bitset
#if (defined _MSC_VER) && (defined _M_ARM || defined _M_ARM64 || defined _M_ARM64EC)
if (IsNegative()) {
return -o.f;
}
#else
// original code:
o.u |= (val & 0x8000U) << 16U; // sign bit
#endif
return o.f;
}
/// Shared implementation between public and internal classes. CRTP pattern.
template <class Derived>
struct BFloat16Impl {
protected:
/// <summary>
/// Converts from float to uint16_t float16 representation
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
static uint16_t ToUint16Impl(float v) noexcept;
/// <summary>
/// Converts bfloat16 to float
/// </summary>
/// <returns>float representation of bfloat16 value</returns>
float ToFloatImpl() const noexcept;
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
uint16_t AbsImpl() const noexcept {
return static_cast<uint16_t>(val & ~kSignMask);
}
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
uint16_t NegateImpl() const noexcept {
return IsNaN() ? val : static_cast<uint16_t>(val ^ kSignMask);
}
public:
// uint16_t special values
static constexpr uint16_t kSignMask = 0x8000U;
static constexpr uint16_t kBiasedExponentMask = 0x7F80U;
static constexpr uint16_t kPositiveInfinityBits = 0x7F80U;
static constexpr uint16_t kNegativeInfinityBits = 0xFF80U;
static constexpr uint16_t kPositiveQNaNBits = 0x7FC1U;
static constexpr uint16_t kNegativeQNaNBits = 0xFFC1U;
static constexpr uint16_t kMaxValueBits = 0x7F7FU;
static constexpr uint16_t kMinValueBits = 0xFF7FU;
static constexpr uint16_t kRoundToNearest = 0x7FFFU;
static constexpr uint16_t kOneBits = 0x3F80U;
static constexpr uint16_t kMinusOneBits = 0xBF80U;
uint16_t val{0};
BFloat16Impl() = default;
/// <summary>
/// Checks if the value is negative
/// </summary>
/// <returns>true if negative</returns>
bool IsNegative() const noexcept {
return static_cast<int16_t>(val) < 0;
}
/// <summary>
/// Tests if the value is NaN
/// </summary>
/// <returns>true if NaN</returns>
bool IsNaN() const noexcept {
return AbsImpl() > kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is finite
/// </summary>
/// <returns>true if finite</returns>
bool IsFinite() const noexcept {
return AbsImpl() < kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents positive infinity.
/// </summary>
/// <returns>true if positive infinity</returns>
bool IsPositiveInfinity() const noexcept {
return val == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents negative infinity
/// </summary>
/// <returns>true if negative infinity</returns>
bool IsNegativeInfinity() const noexcept {
return val == kNegativeInfinityBits;
}
/// <summary>
/// Tests if the value is either positive or negative infinity.
/// </summary>
/// <returns>True if absolute value is infinity</returns>
bool IsInfinity() const noexcept {
return AbsImpl() == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is NaN or zero. Useful for comparisons.
/// </summary>
/// <returns>True if NaN or zero.</returns>
bool IsNaNOrZero() const noexcept {
auto abs = AbsImpl();
return (abs == 0 || abs > kPositiveInfinityBits);
}
/// <summary>
/// Tests if the value is normal (not zero, subnormal, infinite, or NaN).
/// </summary>
/// <returns>True if so</returns>
bool IsNormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) != 0); // is not subnormal (has a non-zero exponent)
}
/// <summary>
/// Tests if the value is subnormal (denormal).
/// </summary>
/// <returns>True if so</returns>
bool IsSubnormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) == 0); // is subnormal (has a zero exponent)
}
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
Derived Abs() const noexcept { return Derived::FromBits(AbsImpl()); }
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
Derived Negate() const noexcept { return Derived::FromBits(NegateImpl()); }
/// <summary>
/// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
/// for two values by or'ing the private bits together and stripping the sign. They are both zero,
/// and therefore equivalent, if the resulting value is still zero.
/// </summary>
/// <param name="lhs">first value</param>
/// <param name="rhs">second value</param>
/// <returns>True if both arguments represent zero</returns>
static bool AreZero(const BFloat16Impl& lhs, const BFloat16Impl& rhs) noexcept {
// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
// for two values by or'ing the private bits together and stripping the sign. They are both zero,
// and therefore equivalent, if the resulting value is still zero.
return static_cast<uint16_t>((lhs.val | rhs.val) & ~kSignMask) == 0;
}
};
template <class Derived>
inline uint16_t BFloat16Impl<Derived>::ToUint16Impl(float v) noexcept {
uint16_t result;
if (std::isnan(v)) {
result = kPositiveQNaNBits;
} else {
auto get_msb_half = [](float fl) {
uint16_t result;
#ifdef __cpp_if_constexpr
if constexpr (detail::endian::native == detail::endian::little) {
#else
if (detail::endian::native == detail::endian::little) {
#endif
std::memcpy(&result, reinterpret_cast<char*>(&fl) + sizeof(uint16_t), sizeof(uint16_t));
} else {
std::memcpy(&result, &fl, sizeof(uint16_t));
}
return result;
};
uint16_t upper_bits = get_msb_half(v);
union {
uint32_t U32;
float F32;
};
F32 = v;
U32 += (upper_bits & 1) + kRoundToNearest;
result = get_msb_half(F32);
}
return result;
}
template <class Derived>
inline float BFloat16Impl<Derived>::ToFloatImpl() const noexcept {
if (IsNaN()) {
return std::numeric_limits<float>::quiet_NaN();
}
float result;
char* const first = reinterpret_cast<char*>(&result);
char* const second = first + sizeof(uint16_t);
#ifdef __cpp_if_constexpr
if constexpr (detail::endian::native == detail::endian::little) {
#else
if (detail::endian::native == detail::endian::little) {
#endif
std::memset(first, 0, sizeof(uint16_t));
std::memcpy(second, &val, sizeof(uint16_t));
} else {
std::memcpy(first, &val, sizeof(uint16_t));
std::memset(second, 0, sizeof(uint16_t));
}
return result;
}
} // namespace onnxruntime_float16

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
/*
* This file defines RunOptions Config Keys and format of the Config Values.
*
* The Naming Convention for a RunOptions Config Key,
* "[Area][.[SubArea1].[SubArea2]...].[Keyname]"
* Such as "ep.cuda.use_arena"
* The Config Key cannot be empty
* The maximum length of the Config Key is 128
*
* The string format of a RunOptions Config Value is defined individually for each Config.
* The maximum length of the Config Value is 1024
*/
// Key for enabling shrinkages of user listed device memory arenas.
// Expects a list of semi-colon separated key value pairs separated by colon in the following format:
// "device_0:device_id_0;device_1:device_id_1"
// No white-spaces allowed in the provided list string.
// Currently, the only supported devices are : "cpu", "gpu" (case sensitive).
// If "cpu" is included in the list, DisableCpuMemArena() API must not be called (i.e.) arena for cpu should be enabled.
// Example usage: "cpu:0;gpu:0" (or) "gpu:0"
// By default, the value for this key is empty (i.e.) no memory arenas are shrunk
static const char* const kOrtRunOptionsConfigEnableMemoryArenaShrinkage = "memory.enable_memory_arena_shrinkage";
// Set to '1' to not synchronize execution providers with CPU at the end of session run.
// Per default it will be set to '0'
// Taking CUDA EP as an example, it omit triggering cudaStreamSynchronize on the compute stream.
static const char* const kOrtRunOptionsConfigDisableSynchronizeExecutionProviders = "disable_synchronize_execution_providers";
// Set HTP performance mode for QNN HTP backend before session run.
// options for HTP performance mode: "burst", "balanced", "default", "high_performance",
// "high_power_saver", "low_balanced", "extreme_power_saver", "low_power_saver", "power_saver",
// "sustained_high_performance". Default to "default".
static const char* const kOrtRunOptionsConfigQnnPerfMode = "qnn.htp_perf_mode";
// Set HTP performance mode for QNN HTP backend post session run.
static const char* const kOrtRunOptionsConfigQnnPerfModePostRun = "qnn.htp_perf_mode_post_run";
// Set RPC control latency for QNN HTP backend
static const char* const kOrtRunOptionsConfigQnnRpcControlLatency = "qnn.rpc_control_latency";
// Set QNN Lora Config File for apply Lora in QNN context binary
static const char* const kOrtRunOptionsConfigQnnLoraConfig = "qnn.lora_config";
// Set graph annotation id for CUDA EP. Use with enable_cuda_graph=true.
// The value should be an integer. If the value is not set, the default value is 0 and
// ORT session only captures one cuda graph before another capture is requested.
// If the value is set to -1, cuda graph capture/replay is disabled in that run.
// User are not expected to set the value to 0 as it is reserved for internal use.
static const char* const kOrtRunOptionsConfigCudaGraphAnnotation = "gpu_graph_id";

View File

@ -0,0 +1,436 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
/*
* This file defines SessionOptions Config Keys and format of the Config Values.
*
* The Naming Convention for a SessionOptions Config Key,
* "[Area][.[SubArea1].[SubArea2]...].[Keyname]"
* Such as "ep.cuda.use_arena"
* The Config Key cannot be empty
* The maximum length of the Config Key is 1024
*
* The string format of a SessionOptions Config Value is defined individually for each Config.
* The maximum length of the Config Value is 8192
*/
// Key for disable PrePacking,
// If the config value is set to "1" then the prepacking is disabled, otherwise prepacking is enabled (default value)
static const char* const kOrtSessionOptionsConfigDisablePrepacking = "session.disable_prepacking";
// A value of "1" means allocators registered in the env will be used. "0" means the allocators created in the session
// will be used. Use this to override the usage of env allocators on a per session level.
static const char* const kOrtSessionOptionsConfigUseEnvAllocators = "session.use_env_allocators";
// Set to 'ORT' (case sensitive) to load an ORT format model.
// If unset, model type will default to ONNX unless inferred from filename ('.ort' == ORT format) or bytes to be ORT
static const char* const kOrtSessionOptionsConfigLoadModelFormat = "session.load_model_format";
// Set to 'ORT' (case sensitive) to save optimized model in ORT format when SessionOptions.optimized_model_path is set.
// If unset, format will default to ONNX unless optimized_model_filepath ends in '.ort'.
static const char* const kOrtSessionOptionsConfigSaveModelFormat = "session.save_model_format";
// If a value is "1", flush-to-zero and denormal-as-zero are applied. The default is "0".
// When multiple sessions are created, a main thread doesn't override changes from succeeding session options,
// but threads in session thread pools follow option changes.
// When ORT runs with OpenMP, the same rule is applied, i.e. the first session option to flush-to-zero and
// denormal-as-zero is only applied to global OpenMP thread pool, which doesn't support per-session thread pool.
// Note that an alternative way not using this option at runtime is to train and export a model without denormals
// and that's recommended because turning this option on may hurt model accuracy.
static const char* const kOrtSessionOptionsConfigSetDenormalAsZero = "session.set_denormal_as_zero";
// It controls to run quantization model in QDQ (QuantizelinearDeQuantizelinear) format or not.
// "0": enable. ORT does fusion logic for QDQ format.
// "1": disable. ORT doesn't do fusion logic for QDQ format.
// Its default value is "0" unless the DirectML execution provider is registered, in which case it defaults to "1".
static const char* const kOrtSessionOptionsDisableQuantQDQ = "session.disable_quant_qdq";
// It controls whether to enable Double QDQ remover and Identical Children Consolidation
// "0": not to disable. ORT does remove the middle 2 Nodes from a Q->(QD->Q)->QD pairs
// "1": disable. ORT doesn't remove the middle 2 Nodes from a Q->(QD->Q)->QD pairs
// Its default value is "0"
static const char* const kOrtSessionOptionsDisableDoubleQDQRemover = "session.disable_double_qdq_remover";
// If set to "1", enables the removal of QuantizeLinear/DequantizeLinear node pairs once all QDQ handling has been
// completed. e.g. If after all QDQ handling has completed and we have -> FloatOp -> Q -> DQ -> FloatOp -> the
// Q -> DQ could potentially be removed. This will provide a performance benefit by avoiding going from float to
// 8-bit and back to float, but could impact accuracy. The impact on accuracy will be model specific and depend on
// other factors like whether the model was created using Quantization Aware Training or Post Training Quantization.
// As such, it's best to test to determine if enabling this works well for your scenario.
// The default value is "0"
// Available since version 1.11.
static const char* const kOrtSessionOptionsEnableQuantQDQCleanup = "session.enable_quant_qdq_cleanup";
// Enable or disable gelu approximation in graph optimization. "0": disable; "1": enable. The default is "0".
// GeluApproximation has side effects which may change the inference results. It is disabled by default due to this.
static const char* const kOrtSessionOptionsEnableGeluApproximation = "optimization.enable_gelu_approximation";
// Enable or disable Cast chain elimination in graph optimization. "0": disable; "1": enable. The default is "0".
// CastElimination with chain elimination has side effects which may change the inference results. It is disabled by default due to this.
static const char* const kOrtSessionOptionsEnableCastChainElimination = "optimization.enable_cast_chain_elimination";
// This setting controls whether to enable AheadOfTime function inlining.
// AOT function inlining examines the graph and attempts to inline as many locally defined functions in the model
// as possible with the help of enabled execution providers.
// This can reduce the number of function calls and improve performance because it is done before
// Level1 optimizers and constant folding. However, under some circumstances, when the EPs are not available,
// one can disable the AOT inlining, produce an optimized model and postpone AOT until run time.
// "0": enable; "1": disable.
// Its default value is "0".
static const char* const kOrtSessionOptionsDisableAheadOfTimeFunctionInlining = "session.disable_aot_function_inlining";
#ifdef ENABLE_TRAINING
// Specifies a path of the file containing a list of memory optimization configurations.
// The value should be a string indicating the file path of the config file.
// The content of the config file is a JSON struct like this:
// [
// "Gelu+Cast+:1:0",
// "Dropout+:1:1"
// ]
// Taking the example of "Gelu+Cast+:1:0",
// > "Gelu+Cast+" is the subgraph string, a valid "subgraph string" should be one subgraph representation
// output by ORT graph transformations.
// > "1" is "optimization strategy", valid values: 0 - disabled, 1 - recompute.
// > "0" is "number of subgraph to apply" which is used to control how many subgraphs to apply optimization,
// to avoid "oversaving" the memory.
static const char* const kOrtSessionOptionsMemoryOptimizerApplyConfig = "optimization.memory_optimizer_config";
// Specifies the config for detecting subgraphs for memory footprint reduction.
// The value should be a string contains int separated using commas. The default value is "0:0".
static const char* const kOrtSessionOptionsMemoryOptimizerProbeConfig = "optimization.enable_memory_probe_recompute_config";
#endif
// This setting if set should contain a comma separated list of optimizers names that should be disabled.
// Optimizers may take time to execute and affect model loading time. If you feel that a specific optimizer
// does not provider runtime benefits, but affects your model loading time you may disable it using this config
// entry. This option is not enabled in ORT_MINIMAL_BUILD build.
// A list of optimizes is available in onnxruntime/core/optimizer/graph_transformer_utils.cc
//
// Default is an empty string which means no optimizers are disabled.
static const char* const kOrtSessionOptionsDisableSpecifiedOptimizers = "optimization.disable_specified_optimizers";
// It controls whether to run graph optimizations in loop or not.
//
// "0": disable. Graph Optimization Loop is disabled.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "No Loop" |
// | |
// X xxxxxxxxxxx X
// ```
// "1": enable. Graph Optimization Loop is enabled, such that, if optimizations at Level 4 are applied then
// the loop will check for any other valid optimization that can happen.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "Loop only depending on Level 4" |
// | |
// ---------------------------------------------------
// ```
// "2": enable. Graph Optimization Loop is enabled, such that, if optimizations at Level 2 or above are applied then
// The loop will check for any other valid optimization that can happen.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "Loop" |
// | |
// ---------------------------------------------------
// ```
// Default value is set to "1".
static const char* const kOrtSessionOptionsGraphOptimizationsLoopLevel = "session.graph_optimizations_loop_level";
// Enable or disable using device allocator for allocating initialized tensor memory. "1": enable; "0": disable. The default is "0".
// Using device allocators means the memory allocation is made using malloc/new.
static const char* const kOrtSessionOptionsUseDeviceAllocatorForInitializers = "session.use_device_allocator_for_initializers";
// Configure whether to allow the inter_op/intra_op threads spinning a number of times before blocking
// "0": thread will block if found no job to run
// "1": thread will spin a number of times before blocking
// The default is "0" when ORT is built with "ORT_CLIENT_PACKAGE_BUILD" and "1" otherwise.
// Thread spinning is disabled by default for client/on-device workloads to reduce cpu utilization and improve power efficiency.
static const char* const kOrtSessionOptionsConfigAllowInterOpSpinning = "session.inter_op.allow_spinning";
static const char* const kOrtSessionOptionsConfigAllowIntraOpSpinning = "session.intra_op.allow_spinning";
// Key for using model bytes directly for ORT format
// If a session is created using an input byte array contains the ORT format model data,
// By default we will copy the model bytes at the time of session creation to ensure the model bytes
// buffer is valid.
// Setting this option to "1" will disable copy the model bytes, and use the model bytes directly. The caller
// has to guarantee that the model bytes are valid until the ORT session using the model bytes is destroyed.
static const char* const kOrtSessionOptionsConfigUseORTModelBytesDirectly = "session.use_ort_model_bytes_directly";
/// <summary>
/// Key for using the ORT format model flatbuffer bytes directly for initializers.
/// This avoids copying the bytes and reduces peak memory usage during model loading and initialization.
/// Requires `session.use_ort_model_bytes_directly` to be true.
/// If set, the flatbuffer bytes provided when creating the InferenceSession MUST remain valid for the entire
/// duration of the InferenceSession.
/// </summary>
static const char* const kOrtSessionOptionsConfigUseORTModelBytesForInitializers =
"session.use_ort_model_bytes_for_initializers";
// This should only be specified when exporting an ORT format model for use on a different platform.
// If the ORT format model will be used on ARM platforms set to "1". For other platforms set to "0"
// Available since version 1.11.
static const char* const kOrtSessionOptionsQDQIsInt8Allowed = "session.qdqisint8allowed";
// x64 SSE4.1/AVX2/AVX512(with no VNNI) has overflow problem with quantizied matrix multiplication with U8S8.
// To avoid this we need to use slower U8U8 matrix multiplication instead. This option, if
// turned on, use slower U8U8 matrix multiplications. Only effective with AVX2 or AVX512
// platforms.
static const char* const kOrtSessionOptionsAvx2PrecisionMode = "session.x64quantprecision";
// Specifies how minimal build graph optimizations are handled in a full build.
// These optimizations are at the extended level or higher.
// Possible values and their effects are:
// "save": Save runtime optimizations when saving an ORT format model.
// "apply": Only apply optimizations available in a minimal build.
// ""/<unspecified>: Apply optimizations available in a full build.
// Available since version 1.11.
static const char* const kOrtSessionOptionsConfigMinimalBuildOptimizations =
"optimization.minimal_build_optimizations";
// Note: The options specific to an EP should be specified prior to appending that EP to the session options object in
// order for them to take effect.
// Specifies a list of stop op types. Nodes of a type in the stop op types and nodes downstream from them will not be
// run by the NNAPI EP.
// The value should be a ","-delimited list of op types. For example, "Add,Sub".
// If not specified, the default set of stop ops is used. To specify an empty stop ops types list and disable stop op
// exclusion, set the value to "".
static const char* const kOrtSessionOptionsConfigNnapiEpPartitioningStopOps = "ep.nnapi.partitioning_stop_ops";
// Enabling dynamic block-sizing for multithreading.
// With a positive value, thread pool will split a task of N iterations to blocks of size starting from:
// N / (num_of_threads * dynamic_block_base)
// As execution progresses, the size will decrease according to the diminishing residual of N,
// meaning the task will be distributed in smaller granularity for better parallelism.
// For some models, it helps to reduce the variance of E2E inference latency and boost performance.
// The feature will not function by default, specify any positive integer, e.g. "4", to enable it.
// Available since version 1.11.
static const char* const kOrtSessionOptionsConfigDynamicBlockBase = "session.dynamic_block_base";
// This option allows to decrease CPU usage between infrequent
// requests and forces any TP threads spinning stop immediately when the last of
// concurrent Run() call returns.
// Spinning is restarted on the next Run() call.
// Applies only to internal thread-pools
static const char* const kOrtSessionOptionsConfigForceSpinningStop = "session.force_spinning_stop";
// "1": all inconsistencies encountered during shape and type inference
// will result in failures.
// "0": in some cases warnings will be logged but processing will continue. The default.
// May be useful to expose bugs in models.
static const char* const kOrtSessionOptionsConfigStrictShapeTypeInference = "session.strict_shape_type_inference";
// "1": every model using a more recent opset than the latest released one will fail
// "0": the model may or may not work if onnxruntime cannot find an implementation, this option
// is used for development purpose.
static const char* const kOrtSessionOptionsConfigStrictAllowReleasedOpsetsOnly = "session.allow_released_opsets_only";
// The file saves configuration for partitioning node among logic streams
static const char* const kNodePartitionConfigFile = "session.node_partition_config_file";
// This Option allows setting affinities for intra op threads.
// Affinity string follows format:
// logical_processor_id,logical_processor_id;logical_processor_id,logical_processor_id
// Semicolon isolates configurations among threads, while comma split processors where ith thread expected to attach to.
// e.g.1,2,3;4,5
// specifies affinities for two threads, with the 1st thread attach to the 1st, 2nd, and 3rd processor, and 2nd thread to the 4th and 5th.
// To ease the configuration, an "interval" is also allowed:
// e.g. 1-8;8-16;17-24
// orders that the 1st thread runs on first eight processors, 2nd thread runs on next eight processors, and so forth.
// Note:
// 1. Once set, the number of thread affinities must equal to intra_op_num_threads - 1, since ort does not set affinity on the main thread which
// is started and managed by the calling app;
// 2. For windows, ort will infer the group id from a logical processor id, for example, assuming there are two groups with each has 64 logical processors,
// an id of 64 will be inferred as the last processor of the 1st group, while 65 will be interpreted as the 1st processor of the second group.
// Hence 64-65 is an invalid configuration, because a windows thread cannot be attached to processors across group boundary.
static const char* const kOrtSessionOptionsConfigIntraOpThreadAffinities = "session.intra_op_thread_affinities";
// This option will dump out the model to assist debugging any issues with layout transformation,
// and is primarily intended for developer usage. It is only relevant if an execution provider that requests
// NHWC layout is enabled such as NNAPI, XNNPACK or QNN.
//
// Default is off. Set to "1" to enable.
//
// If modified by layout transformation the model will be dumped after these steps:
// 1) insertion of the layout transformation Transpose nodes
// 2) after those are optimized using the transpose optimizer,
// 3) after the L1 transformers are applied to the updated graph.
// The model will be saved to filename post_layout_transform_step_<step_number>.onnx.
static const char* const kDebugLayoutTransformation = "session.debug_layout_transformation";
// Graph nodes that are not supported by the execution providers (EPs) explicitly added to the session are
// assigned (i.e., "fallback") to the CPU EP by default.
//
// This option allows the user to disable the fallback of unsupported graph nodes to the CPU EP.
// If this option is set to "1", session creation will fail if the execution providers other than the CPU EP cannot
// fully support all of the nodes in the graph.
//
// It is invalid to set this option and explicitly add the CPU EP to the session. In this case, session creation
// will also fail with an error.
//
// Option values:
// - "0": CPU EP fallback is not disabled. [DEFAULT]
// - "1": CPU EP fallback is disabled.
static const char* const kOrtSessionOptionsDisableCPUEPFallback = "session.disable_cpu_ep_fallback";
// Use this config when serializing a large model after optimization to specify an external initializers file
static const char* const kOrtSessionOptionsOptimizedModelExternalInitializersFileName =
"session.optimized_model_external_initializers_file_name";
// Use this config to control the minimum size of the initializer when externalizing it during serialization
static const char* const kOrtSessionOptionsOptimizedModelExternalInitializersMinSizeInBytes =
"session.optimized_model_external_initializers_min_size_in_bytes";
// When loading model from memory buffer and the model has external initializers
// Use this config to set the external data file folder path
// All external data files should be in the same folder
static const char* const kOrtSessionOptionsModelExternalInitializersFileFolderPath =
"session.model_external_initializers_file_folder_path";
// Use this config when saving pre-packed constant initializers to an external data file.
// This allows you to memory map pre-packed initializers on model load and leave it to
// to the OS the amount of memory consumed by the pre-packed initializers. Otherwise,
// pre-packed data resides on the heap.
//
// - "0": Default is not save pre-packed initializers to a data file.
// - "1": Save pre-packed constant initializers to an external data file.
// Sample usage: sess_options.add_session_config_entry(kOrtSessionOptionsSavePrePackedConstantInitializers, "1")
static const char* const kOrtSessionOptionsSavePrePackedConstantInitializers =
"session.save_external_prepacked_constant_initializers";
// Use this config when you want to collect memory stats for each node in the graph.
// The file format is a CSV file with the following columns:
// The file will be created if it does not exist, and will be overwritten if it does.
//
// The content of the file can be used to estimate memory requirements at run time including
// the temporary allocations. This operation is preferably done on a CPU device, as the model may exceed
// device memory limits in constrained environments. When enabling this option, it is important to disable
// memory patterns, as they tend to allocate large blocks to avoid fragmentation and accommodate needs of multiple
// kernels. Memory patterns may make it difficult to allocate on a device with limited memory.
//
// The collected stats then can be used to partition the graph among the devices in a way that only the
// required memory is allocated on each device.
//
// node_name, initializers_memory, dynamic_outputs_sizes, temp_allocations_size
//
// - "full path to file": there is not a default for this option. If the file can not be opened for writing, an error will be returned.
static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "session.collect_node_memory_stats_to_file";
/// This is a composite CSV setting formatted as "memory limit in kb,file name for collected stats"
/// "limit > 0": enables Capacity Aware Partitioning for Cuda EP. `limit` is optional and when absent
/// the provider may attempt to figure out the memory available automatically.
/// The setting with no limit is expected to look like: ",file name for collected stats"
/// The EP will place nodes on device "file name" :
/// this file is expected to be found at the same folder with the model. The file contains
/// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above)
static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings =
"session.resource_cuda_partitioning_settings";
// Enable EP context feature to dump the partitioned graph which includes the EP context into Onnx file.
// The dumped Onnx model with EP context can be used for future inference to avoid the EP graph partitioning/compile overhead.
// "0": disable. (default)
// "1": enable.
static const char* const kOrtSessionOptionEpContextEnable = "ep.context_enable";
// Specify the file path for the Onnx model which has EP context.
// Default to original_file_name_ctx.onnx if not specified
// Folder is not a valid option
static const char* const kOrtSessionOptionEpContextFilePath = "ep.context_file_path";
// Flag to specify whether to dump the EP context into the Onnx model.
// "0": dump the EP context into separate file, keep the file name in the Onnx model. (default).
// "1": dump the EP context into the Onnx model.
static const char* const kOrtSessionOptionEpContextEmbedMode = "ep.context_embed_mode";
// Specify the EPContext node name prefix to make it unique
// in case user need to merge/connect multiple EPContext nodes in one model
static const char* const kOrtSessionOptionEpContextNodeNamePrefix = "ep.context_node_name_prefix";
// Share EP related resources across sessions
static const char* const kOrtSessionOptionShareEpContexts = "ep.share_ep_contexts";
// Stop to share EP related resources across sessions from then on
static const char* const kOrtSessionOptionStopShareEpContexts = "ep.stop_share_ep_contexts";
// Used only for context model generation.
// This configuration is used when some nodes are partitioned on the CPU EP and those nodes have external initializers.
// When generating the EP context model, the new model should not rely on the old external data file used by the source ONNX model.
// Use this setting when dumping the EP context model with an external initializers file.
// If specified, all initializers will be placed inside the external data file.
// Otherwise, all initializers will be embedded inside the generated ONNX file.
// By default, this option is not set, meaning all initializers will be included within the ONNX file.
static const char* const kOrtSessionOptionsEpContextModelExternalInitializersFileName =
"ep.context_model_external_initializers_file_name";
// Gemm fastmath mode provides fp32 gemm acceleration with bfloat16 based matmul.
// Option values:
// - "0": Gemm FastMath mode is not enabled. [DEFAULT]
// - "1": Gemm FastMath mode is enabled.
static const char* const kOrtSessionOptionsMlasGemmFastMathArm64Bfloat16 = "mlas.enable_gemm_fastmath_arm64_bfloat16";
// Use LUT (Lookup Table) based GEMM for quantized models when available.
// Option values:
// - "0": Do not use LUT based GEMM. [DEFAULT]
// - "1": Use LUT based GEMM when available.
static const char* const kOrtSessionOptionsMlasLutGemm = "mlas.use_lut_gemm";
// When converting DQ + MatMul -> MatMulNBits, the accuracy level of the MatMulNBits is controlled by this option.
// Refer to MatMulNBits op schema for more details.
// If not provided, default is 4.
static const char* const kOrtSessionOptionsQDQMatMulNBitsAccuracyLevel = "session.qdq_matmulnbits_accuracy_level";
// Enable the DQ->MatMulNBits fusion graph transformer.
// "0": disabled (default). "1": enabled.
// This is typically set automatically by InferenceSession when the NvTensorRTRTX EP is registered.
static const char* const kOrtSessionOptionsEnableDQMatMulNBitsFusion = "session.enable_dq_matmulnbits_fusion";
// THIS OPTION IS NOT A REGULAR SESSION OPTION SINCE IT CAN BE MODIFIED AT ANY TIME
// Meant to be used with SetEpDynamicOptions
// Specify the type of workload for this session.
// "Default": OS determines the scheduling priority and processor performance to service this workload. [Default]
// "Efficient": OS treats this workload is efficiency oriented with low scheduling priority and efficient processor performance.
static const char* const kOrtEpDynamicOptionsWorkloadType = "ep.dynamic.workload_type";
// Disables model compilation during session initialization.
//
// If this option is set to "1", inference session creation will fail with error code ORT_MODEL_REQUIRES_COMPILATION
// if compilation is required to run the model on any Execution Provider added to the session.
// Only the following kinds of models are valid when this option is set to "1":
// - Pre-compiled models that have EPContext nodes for the compiling Execution Providers in the session.
// - Non-compiled models that run only on non-compiling Execution Providers, like CPU EP.
//
// See \href https://onnxruntime.ai/docs/execution-providers/EP-Context-Design.html for details about
// compiled models with EPContext nodes.
//
// Option values:
// - "0": EP compile is not disabled. [DEFAULT]
// - "1": EP compile is disabled.
static const char* const kOrtSessionOptionsDisableModelCompile = "session.disable_model_compile";
// Controls behavior when compiled model compatibility is SUPPORTED_PREFER_RECOMPILATION.
// "0": Allow execution with suboptimal performance. [DEFAULT]
// "1": Fail session creation to require recompilation for optimal performance.
// Note: UNSUPPORTED models always fail regardless of this setting.
static const char* const kOrtSessionOptionsFailOnSuboptimalCompiledModel =
"session.fail_on_suboptimal_compiled_model";
// THIS OPTION IS NOT A REGULAR SESSION OPTION SINCE IT CAN BE MODIFIED AT ANY TIME
// Meant to be used with SetEpDynamicOptions
// options for HTP performance mode: "burst", "balanced", "default", "high_performance",
// "high_power_saver", "low_balanced", "extreme_power_saver", "low_power_saver", "power_saver",
// "sustained_high_performance". Default to "default".
static const char* const kOrtEpDynamicOptionsQnnHtpPerformanceMode = "ep.dynamic.qnn_htp_performance_mode";
// Enables the session to record information about the subgraphs/nodes assigned to execution providers.
// When enabled, an application may call Session_GetEpGraphAssignmentInfo() to retrieve the information.
//
// Option values:
// - "0": Recording of EP graph assignment information is disabled. [DEFAULT]
// - "1": Recording of EP graph assignment information is enabled.
static const char* const kOrtSessionOptionsRecordEpGraphAssignmentInfo = "session.record_ep_graph_assignment_info";

236
dist/jni/kazeia_mel.cpp vendored Normal file
View File

@ -0,0 +1,236 @@
// Implementation kazeia_mel — voir kazeia_mel.h pour l'API publique.
#include "kazeia_mel.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace kazeia_mel {
// ============================================================================
// Configurations pré-faites
// ============================================================================
MelConfig config_qwen3_tts_speaker() {
MelConfig c;
c.sample_rate = 24000;
c.n_fft = 1024;
c.hop_size = 256;
c.win_size = 1024;
c.n_mels = 128;
c.pad_amount = (1024 - 256) / 2; // 384, reflect (TTS convention)
c.fixed_n_frames = 0;
c.clip_eps = 1e-5f;
c.window = WindowKind::HANN_SYMMETRIC;
c.comp = Compression::LOG_NATURAL;
c.post = PostNorm::NONE;
return c;
}
MelConfig config_whisper() {
MelConfig c;
c.sample_rate = 16000;
c.n_fft = 400;
c.hop_size = 160;
c.win_size = 400;
c.n_mels = 80;
c.pad_amount = 200; // n_fft/2 (HF whisper)
c.fixed_n_frames = 3000; // 30s padding fixe
c.clip_eps = 1e-10f;
c.window = WindowKind::HANN_PERIODIC;
c.comp = Compression::LOG10;
c.post = PostNorm::WHISPER;
return c;
}
// ============================================================================
// Helpers internes
// ============================================================================
// FFT radix-2 in-place (n doit être puissance de 2). Si n_fft pas puissance de 2
// (cas Whisper N_FFT=400), on zero-pad jusqu'à la puissance de 2 supérieure.
static void fft_radix2(float* real, float* imag, int n) {
int j = 0;
for (int i = 1; i < n; i++) {
int bit = n >> 1;
while (j & bit) { j ^= bit; bit >>= 1; }
j ^= bit;
if (i < j) { std::swap(real[i], real[j]); std::swap(imag[i], imag[j]); }
}
for (int len = 2; len <= n; len <<= 1) {
int half = len / 2;
double angle = -2.0 * M_PI / len;
float wR = (float)std::cos(angle), wI = (float)std::sin(angle);
for (int i = 0; i < n; i += len) {
float cR = 1.f, cI = 0.f;
for (int k = 0; k < half; k++) {
float tR = cR * real[i+k+half] - cI * imag[i+k+half];
float tI = cR * imag[i+k+half] + cI * real[i+k+half];
real[i+k+half] = real[i+k] - tR;
imag[i+k+half] = imag[i+k] - tI;
real[i+k] += tR;
imag[i+k] += tI;
float ncR = cR * wR - cI * wI;
cI = cR * wI + cI * wR;
cR = ncR;
}
}
}
}
static int next_pow2(int n) {
int p = 1; while (p < n) p <<= 1; return p;
}
static std::vector<float> make_window(int n, WindowKind kind) {
std::vector<float> w(n);
if (kind == WindowKind::HANN_SYMMETRIC) {
// torch.hann_window(N) default = symmetric : 0.5 * (1 - cos(2*pi*n/(N-1)))
for (int i = 0; i < n; i++)
w[i] = 0.5f * (1.0f - std::cos(2.0f * (float)M_PI * (float)i / (float)(n - 1)));
} else {
// Periodic (HF whisper, numpy default) : 0.5 * (1 - cos(2*pi*n/N))
for (int i = 0; i < n; i++)
w[i] = 0.5f * (1.0f - std::cos(2.0f * (float)M_PI * (float)i / (float)n));
}
return w;
}
// Pad reflect : miroir interne (n'inclut PAS le sample bord). Équivalent torch reflect.
static std::vector<float> pad_reflect(const std::vector<float>& x, int padding) {
const int n = (int)x.size();
std::vector<float> out((size_t)n + 2 * padding);
for (int i = 0; i < padding; i++) out[i] = x[padding - i];
for (int i = 0; i < n; i++) out[padding + i] = x[i];
for (int i = 0; i < padding; i++) out[padding + n + i] = x[n - 2 - i];
return out;
}
// ============================================================================
// load_mel_basis : fichier binaire f32 [n_mels, n_fft/2+1]
// ============================================================================
bool load_mel_basis(const char * path, const MelConfig & cfg, std::vector<float> & out) {
const int fft_bins = cfg.n_fft / 2 + 1;
const size_t expected = (size_t)cfg.n_mels * fft_bins;
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) { fprintf(stderr, "kazeia_mel::load_mel_basis: open %s FAIL\n", path); return false; }
size_t n = (size_t)f.tellg() / sizeof(float);
if (n != expected) {
fprintf(stderr, "kazeia_mel::load_mel_basis: %s contient %zu f32, attendu %zu (=%d mels x %d bins)\n",
path, n, expected, cfg.n_mels, fft_bins);
return false;
}
f.seekg(0);
out.assign(expected, 0.0f);
f.read((char*)out.data(), expected * sizeof(float));
return (bool)f;
}
// ============================================================================
// compute : STFT (radix-2 zero-pad) + projection mel + log + post-norm
// ============================================================================
std::vector<float> compute(const std::vector<float> & wav_in,
const std::vector<float> & mel_basis,
const MelConfig & cfg,
int & out_T) {
const int fft_bins = cfg.n_fft / 2 + 1;
const int pad_amount = (cfg.pad_amount >= 0) ? cfg.pad_amount : (cfg.n_fft - cfg.hop_size) / 2;
// Optionnel : padding audio à T_fixe (Whisper 30s)
std::vector<float> wav = wav_in;
if (cfg.fixed_n_frames > 0) {
int target_samples = cfg.fixed_n_frames * cfg.hop_size;
if ((int)wav.size() > target_samples) wav.resize(target_samples);
else if ((int)wav.size() < target_samples) wav.resize(target_samples, 0.0f);
}
auto pad = pad_reflect(wav, pad_amount);
auto hann = make_window(cfg.win_size, cfg.window);
const int len_pad = (int)pad.size();
if (len_pad < cfg.n_fft) {
fprintf(stderr, "kazeia_mel::compute: wav trop court (%d < %d)\n", len_pad, cfg.n_fft);
out_T = 0; return {};
}
// T_compute : sans fixed_n_frames -> calcule autant que possible (center=False).
// Avec fixed_n_frames -> exactement N_FRAMES (le padding ci-dessus garantit la place).
int T;
if (cfg.fixed_n_frames > 0) T = cfg.fixed_n_frames;
else T = (len_pad - cfg.n_fft) / cfg.hop_size + 1;
out_T = T;
// FFT en puissance de 2 >= n_fft (Whisper N_FFT=400 -> FFT 512).
const int fftN = next_pow2(cfg.n_fft);
std::vector<float> spec((size_t)fft_bins * T, 0.0f);
std::vector<float> re(fftN), im(fftN);
for (int t = 0; t < T; t++) {
const int off = t * cfg.hop_size;
// Window sur win_size samples, zero-pad jusqu'à fftN
for (int i = 0; i < cfg.win_size; i++) {
re[i] = pad[off + i] * hann[i];
im[i] = 0.0f;
}
for (int i = cfg.win_size; i < fftN; i++) { re[i] = 0.0f; im[i] = 0.0f; }
fft_radix2(re.data(), im.data(), fftN);
// Power spectrum [0..n_fft/2]
for (int k = 0; k < fft_bins; k++) {
spec[(size_t)k * T + t] = re[k]*re[k] + im[k]*im[k];
}
}
// mel = basis [n_mels, fft_bins] @ spec [fft_bins, T] -> [n_mels, T]
std::vector<float> mel((size_t)cfg.n_mels * T, 0.0f);
for (int m = 0; m < cfg.n_mels; m++) {
for (int t = 0; t < T; t++) {
float s = 0.0f;
for (int k = 0; k < fft_bins; k++) {
s += mel_basis[m * fft_bins + k] * spec[(size_t)k * T + t];
}
mel[(size_t)m * T + t] = std::max(s, cfg.clip_eps);
}
}
// Compression (log naturel ou log10) + sqrt si TTS (variante: TTS prend sqrt magnitude
// AVANT projection mel ; Whisper prend power direct projetée puis log10).
// Distinction empirique du speaker encoder Qwen3-TTS : il calcule magnitude = sqrt(re²+im²+1e-9)
// puis projette puis log. Pour rester strictement bit-correct avec le code spk d'origine,
// on adapte si compression == LOG_NATURAL : sqrt avant log.
if (cfg.comp == Compression::LOG_NATURAL) {
// TTS : sqrt(power+1e-9) puis mel_proj puis log(max(., eps))
// Refait correctement : revert la projection power, refais en magnitude.
// Implémenter ainsi pour bit-match :
std::vector<float> mag_spec((size_t)fft_bins * T);
for (int k = 0; k < fft_bins; k++)
for (int t = 0; t < T; t++)
mag_spec[(size_t)k * T + t] = std::sqrt(spec[(size_t)k * T + t] + 1e-9f);
for (int m = 0; m < cfg.n_mels; m++) {
for (int t = 0; t < T; t++) {
float s = 0.0f;
for (int k = 0; k < fft_bins; k++)
s += mel_basis[m * fft_bins + k] * mag_spec[(size_t)k * T + t];
mel[(size_t)m * T + t] = std::log(std::max(s, cfg.clip_eps));
}
}
} else { // LOG10 (Whisper)
for (auto & v : mel) v = std::log10(v);
}
// Post-norm (Whisper : clamp à max-8, puis (x+4)/4)
if (cfg.post == PostNorm::WHISPER) {
float maxv = *std::max_element(mel.begin(), mel.end());
float floor_v = maxv - 8.0f;
for (auto & v : mel) {
if (v < floor_v) v = floor_v;
v = (v + 4.0f) * 0.25f;
}
}
return mel;
}
} // namespace kazeia_mel

63
dist/jni/kazeia_mel.h vendored Normal file
View File

@ -0,0 +1,63 @@
// Mel spectrogram extractor unifié Kazeia-Engine.
// Couvre Whisper (16kHz, 80 mel, log10 + Whisper norm) et Qwen3-TTS speaker encoder
// (24kHz, 128 mel, log naturel + clip 1e-5).
//
// Bit-correct vs librosa/torch reference pour les deux profils.
//
// Layout sortie : [N_MELS, T] = data[mel * T + frame] (PyTorch [B, C, T] / ggml
// ne[0]=T fastest). Compatible directement avec speaker_encoder_encode_waveform()
// et Whisper encoder ONNX (qui veut [1, n_mels, n_frames] fp16).
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace kazeia_mel {
enum class WindowKind {
HANN_SYMMETRIC, // torch.hann_window(N) = 0.5*(1-cos(2*pi*n/(N-1))) (TTS)
HANN_PERIODIC, // numpy/HF whisper = 0.5*(1-cos(2*pi*n/N)) (STT)
};
enum class Compression {
LOG_NATURAL, // log(max(mel, eps)) (TTS Qwen3-TTS)
LOG10, // log10(max(mel, eps)) (STT Whisper)
};
enum class PostNorm {
NONE, // pas de post-process (TTS)
WHISPER, // clamp à max-8, puis (x+4)/4 (STT Whisper)
};
struct MelConfig {
int sample_rate = 16000;
int n_fft = 400;
int hop_size = 160;
int win_size = 400;
int n_mels = 80;
int pad_amount = -1; // -1 -> auto : whisper n_fft/2, tts (n_fft-hop)/2
int fixed_n_frames = 0; // 0 -> variable ; >0 -> pad audio à T fixe (whisper=3000=30s)
float clip_eps = 1e-5f; // valeur min avant log
WindowKind window = WindowKind::HANN_SYMMETRIC;
Compression comp = Compression::LOG_NATURAL;
PostNorm post = PostNorm::NONE;
};
// Configs pré-faites — utiliser comme défauts puis customiser au besoin.
MelConfig config_qwen3_tts_speaker(); // SR=24000, N_FFT=1024, hop=256, n_mels=128, log naturel
MelConfig config_whisper(); // SR=16000, N_FFT=400, hop=160, n_mels=80, log10 + WhisperNorm
// Charge mel_basis [n_mels, n_fft/2+1] depuis un fichier binaire f32 (librosa pré-calculé
// ou mel_filters.json converti). Le caller fournit le path et la lib vérifie la taille
// attendue n_mels * (n_fft/2+1).
bool load_mel_basis(const char * path, const MelConfig & cfg, std::vector<float> & out);
// Compute mel spectrogram. wav = mono f32 [-1..1] @ cfg.sample_rate. mel_basis attendu
// dans le layout librosa [n_mels, n_fft/2+1]. Retourne mel [n_mels, T] dans le layout
// data[mel * T + frame]. out_T renvoie le nombre de frames calculés.
std::vector<float> compute(const std::vector<float> & wav,
const std::vector<float> & mel_basis,
const MelConfig & cfg,
int & out_T);
} // namespace kazeia_mel

View File

@ -15,6 +15,7 @@
// Compile avec -DSPK_STANDALONE pour le binaire CLI : // Compile avec -DSPK_STANDALONE pour le binaire CLI :
// usage : kazeia_speaker_encode <gguf> <mel_basis.bin> <ref.wav> <out.bin> // usage : kazeia_speaker_encode <gguf> <mel_basis.bin> <ref.wav> <out.bin>
#include "speaker_encoder.h" #include "speaker_encoder.h"
#include "kazeia_mel.h"
#include "ggml.h" #include "ggml.h"
#include "gguf.h" #include "gguf.h"
#include "ggml-cpu.h" #include "ggml-cpu.h"
@ -29,111 +30,11 @@
#include <unordered_map> #include <unordered_map>
// ============================================================================ // ============================================================================
// Constantes mel (cf /opt/Kazeia/qnn_venv/.../modeling_qwen3_tts.py mel_spectrogram) // Constantes mel : maintenant centralisées dans kazeia_mel::config_qwen3_tts_speaker()
// ============================================================================ // ============================================================================
static const int SAMPLE_RATE = 24000; static const int SAMPLE_RATE = 24000;
static const int N_FFT = 1024;
static const int HOP_SIZE = 256;
static const int WIN_SIZE = 1024;
static const int N_MELS = 128; static const int N_MELS = 128;
static const int FFT_BINS = N_FFT / 2 + 1; // 513 static const int FFT_BINS = 513; // n_fft/2+1 pour n_fft=1024
static const float MEL_CLIP_VAL = 1e-5f;
// ============================================================================
// FFT radix-2 (in-place, n doit être puissance de 2). Identique à mel_extractor.cpp.
// ============================================================================
static void fft_radix2(float* real, float* imag, int n) {
// Bit-reversal
int j = 0;
for (int i = 1; i < n; i++) {
int bit = n >> 1;
while (j & bit) { j ^= bit; bit >>= 1; }
j ^= bit;
if (i < j) { std::swap(real[i], real[j]); std::swap(imag[i], imag[j]); }
}
for (int len = 2; len <= n; len <<= 1) {
int half = len / 2;
double angle = -2.0 * M_PI / len;
float wR = (float)std::cos(angle), wI = (float)std::sin(angle);
for (int i = 0; i < n; i += len) {
float cR = 1.f, cI = 0.f;
for (int k = 0; k < half; k++) {
float tR = cR * real[i+k+half] - cI * imag[i+k+half];
float tI = cR * imag[i+k+half] + cI * real[i+k+half];
real[i+k+half] = real[i+k] - tR;
imag[i+k+half] = imag[i+k] - tI;
real[i+k] += tR;
imag[i+k] += tI;
float ncR = cR * wR - cI * wI;
cI = cR * wI + cI * wR;
cR = ncR;
}
}
}
}
// Hann window (cosine), torch.hann_window(N) = 0.5*(1 - cos(2π * n / (N-1)))
static std::vector<float> make_hann(int n) {
std::vector<float> w(n);
for (int i = 0; i < n; i++)
w[i] = 0.5f * (1.0f - std::cos(2.0f * (float)M_PI * (float)i / (float)(n - 1)));
return w;
}
// Pad reflect : insère padding samples sur chaque côté, miroir interne (sans répéter
// la 1ère/dernière). Équivalent torch.nn.functional.pad(..., mode="reflect").
static std::vector<float> pad_reflect(const std::vector<float>& x, int padding) {
const int n = (int)x.size();
std::vector<float> out((size_t)n + 2 * padding);
for (int i = 0; i < padding; i++) out[i] = x[padding - i]; // miroir gauche
for (int i = 0; i < n; i++) out[padding + i] = x[i]; // milieu
for (int i = 0; i < padding; i++) out[padding + n + i] = x[n - 2 - i]; // miroir droite
return out;
}
// Compute mel-spectrogram [n_mels=128, T] depuis waveform mono 24kHz.
// Réplique exactement qwen_tts.core.models.modeling_qwen3_tts.mel_spectrogram.
static std::vector<float> mel_spectrogram(const std::vector<float>& wav,
const std::vector<float>& mel_basis, // [128, 513]
int& out_T) {
const int padding = (N_FFT - HOP_SIZE) / 2; // 384
auto pad = pad_reflect(wav, padding);
auto hann = make_hann(WIN_SIZE);
// T = floor((len_padded - n_fft) / hop) + 1 (center=False)
const int len_pad = (int)pad.size();
if (len_pad < N_FFT) { fprintf(stderr, "wav too short (%d < %d)\n", len_pad, N_FFT); return {}; }
const int T = (len_pad - N_FFT) / HOP_SIZE + 1;
out_T = T;
std::vector<float> spec(FFT_BINS * T, 0.0f); // [513, T] magnitude
std::vector<float> re(N_FFT), im(N_FFT);
for (int t = 0; t < T; t++) {
const int off = t * HOP_SIZE;
for (int i = 0; i < N_FFT; i++) {
re[i] = pad[off + i] * hann[i];
im[i] = 0.0f;
}
fft_radix2(re.data(), im.data(), N_FFT);
// Magnitude sqrt(re² + im² + 1e-9), onesided -> bins 0..N_FFT/2
for (int k = 0; k < FFT_BINS; k++) {
float m = re[k]*re[k] + im[k]*im[k] + 1e-9f;
spec[k * T + t] = std::sqrt(m);
}
}
// mel = mel_basis [128, 513] @ spec [513, T] -> [128, T]
std::vector<float> mel(N_MELS * T, 0.0f);
for (int m = 0; m < N_MELS; m++) {
for (int t = 0; t < T; t++) {
float s = 0.0f;
for (int k = 0; k < FFT_BINS; k++)
s += mel_basis[m * FFT_BINS + k] * spec[k * T + t];
mel[m * T + t] = std::log(std::max(s, MEL_CLIP_VAL)); // dynamic_range_compression
}
}
return mel;
}
// ============================================================================ // ============================================================================
// Forward ECAPA-TDNN ggml C++ — bit-match qwen_tts.modeling_qwen3_tts // Forward ECAPA-TDNN ggml C++ — bit-match qwen_tts.modeling_qwen3_tts
@ -540,7 +441,8 @@ SpeakerEncoder * speaker_encoder_load(const char * gguf_path, const char * mel_b
std::vector<float> speaker_encoder_encode_waveform(SpeakerEncoder * spk, const std::vector<float> & wav) { std::vector<float> speaker_encoder_encode_waveform(SpeakerEncoder * spk, const std::vector<float> & wav) {
if (!spk) return {}; if (!spk) return {};
int T_mel = 0; int T_mel = 0;
auto mel = mel_spectrogram(wav, spk->mel_basis, T_mel); const auto cfg = kazeia_mel::config_qwen3_tts_speaker();
auto mel = kazeia_mel::compute(wav, spk->mel_basis, cfg, T_mel);
if (mel.empty()) return {}; if (mel.empty()) return {};
return spk_forward(*spk, mel, T_mel); return spk_forward(*spk, mel, T_mel);
} }

167
dist/jni/stt_engine.cpp vendored Normal file
View File

@ -0,0 +1,167 @@
// Implementation stt_engine — voir stt_engine.h pour l'API publique.
//
// S1.3 (cette session) : squelette compilable, VAD complet, mel via kazeia_mel.
// stt_engine_load / transcribe / free : stubs renvoyant err=-99 jusqu'au port ORT
// QNN en S2.
#include "stt_engine.h"
#include "kazeia_mel.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
#include <string>
// ============================================================================
// VAD RMS énergie (port C++ de VadStage.kt)
// ============================================================================
//
// État interne : compteur de frames parole / silence consécutifs, sliding sur
// chunks PCM. L'appelant push des chunks de taille frame_size; chaque push fait
// avancer l'état. is_speech / is_end_of_speech consultent l'état.
struct SttVadState {
int sample_rate;
int frame_size; // 1600 = 100 ms @ 16 kHz
int rms_threshold; // 150 sur PCM16 brut (prod KazeiaService)
int min_speech_frames; // 3 -> 300 ms de parole pour déclencher
int silence_end_frames; // 8 -> 800 ms de silence pour finir
int speech_count = 0;
int silence_count = 0;
bool in_speech = false;
bool end_of_speech = false;
};
SttVadState * stt_vad_new(int sample_rate, int frame_size, int rms_threshold,
int min_speech_frames, int silence_end_frames) {
auto * s = new SttVadState();
s->sample_rate = sample_rate;
s->frame_size = frame_size;
s->rms_threshold = rms_threshold;
s->min_speech_frames = min_speech_frames;
s->silence_end_frames = silence_end_frames;
return s;
}
void stt_vad_reset(SttVadState * s) {
if (!s) return;
s->speech_count = 0;
s->silence_count = 0;
s->in_speech = false;
s->end_of_speech = false;
}
void stt_vad_free(SttVadState * s) { delete s; }
bool stt_vad_is_speech(const SttVadState * s) { return s && s->in_speech; }
bool stt_vad_is_end_of_speech(const SttVadState * s) { return s && s->end_of_speech; }
// Calcule RMS d'un buffer PCM16, renvoie l'énergie en unités PCM brutes
// (compatible seuil 150 prod).
static float rms_pcm16(const int16_t * pcm, int n) {
if (n <= 0) return 0.0f;
double acc = 0.0;
for (int i = 0; i < n; ++i) { double v = (double)pcm[i]; acc += v * v; }
return (float)std::sqrt(acc / (double)n);
}
bool stt_vad_push(SttVadState * s, const int16_t * pcm, int n_samples) {
if (!s || !pcm || n_samples <= 0) return false;
s->end_of_speech = false;
// Traite par chunks de frame_size. Si n_samples < frame_size, on accumulera
// mentalement mais ici on calcule simplement la RMS sur ce qu'on a (l'app
// appelante pousse des chunks pleins de 100 ms en pratique).
int offset = 0;
while (offset + s->frame_size <= n_samples) {
float r = rms_pcm16(pcm + offset, s->frame_size);
if (r >= (float)s->rms_threshold) {
s->speech_count += 1;
s->silence_count = 0;
if (!s->in_speech && s->speech_count >= s->min_speech_frames) {
s->in_speech = true;
}
} else {
s->silence_count += 1;
// Pas de reset speech_count tant qu'on est in_speech, c'est l'enchainement
// silence consécutifs qui clôt.
if (s->in_speech && s->silence_count >= s->silence_end_frames) {
s->in_speech = false;
s->end_of_speech = true;
s->speech_count = 0;
} else if (!s->in_speech) {
s->speech_count = 0; // reset compteur si on n'a pas atteint le seuil
}
}
offset += s->frame_size;
}
return true;
}
// ============================================================================
// SttEngine (stubs — port ORT QNN en S2)
// ============================================================================
struct SttEngine {
SttEngineLoadCfg cfg;
std::vector<float> mel_basis; // [80, 201] librosa whisper
// S2 :
// void * encoder_session; // Ort::Session*
// void * decoder_session;
// std::vector<int> vocab_ids;
// std::unordered_map<std::string,int> token_to_id;
bool loaded = false;
};
SttEngine * stt_engine_load(const SttEngineLoadCfg & cfg) {
if (!cfg.model_dir) {
fprintf(stderr, "stt_engine_load: model_dir requis\n"); return nullptr;
}
auto * eng = new SttEngine();
eng->cfg = cfg;
// Tente charger mel_filters.bin (préféré) ou mel_filters.json (sera porté en S2).
std::string mel_path = std::string(cfg.model_dir) + "/mel_filters.bin";
auto mel_cfg = kazeia_mel::config_whisper();
if (!kazeia_mel::load_mel_basis(mel_path.c_str(), mel_cfg, eng->mel_basis)) {
fprintf(stderr, "stt_engine_load: mel_filters.bin absent ou mauvaise taille à %s\n",
mel_path.c_str());
// Pas fatal : à S2 on chargera depuis mel_filters.json. Pour S1.3 on continue
// pour permettre les tests VAD.
}
eng->loaded = true;
return eng;
}
SttTranscribeResult stt_engine_transcribe(SttEngine * eng, const SttTranscribeCfg & cfg) {
SttTranscribeResult R{};
if (!eng || !eng->loaded || !cfg.pcm16 || cfg.n_samples <= 0) {
R.err = -1; return R;
}
// Étape 1 : mel via kazeia_mel (Whisper config). Implémentée en S1.3 pour exercer
// la lib partagée ; le encoder/decoder ORT viennent en S2.
std::vector<float> wav((size_t)cfg.n_samples);
for (int i = 0; i < cfg.n_samples; ++i) wav[i] = (float)cfg.pcm16[i] / 32768.0f;
if (eng->mel_basis.empty()) {
fprintf(stderr, "stt_engine_transcribe: mel_basis non chargé (mel_filters.bin manquant)\n");
R.err = -2; return R;
}
auto mel_cfg = kazeia_mel::config_whisper();
int T = 0;
auto mel = kazeia_mel::compute(wav, eng->mel_basis, mel_cfg, T);
if (mel.empty()) { R.err = -3; return R; }
R.mel_ms = 0; // TODO: timer
// Étape 2..N : encoder NPU + decoder loop + tokenizer BPE -> S2.
fprintf(stderr, "stt_engine_transcribe: stubs (port S2 requis). mel %d frames OK.\n", T);
R.err = -99; // not implemented yet
R.detected_language = cfg.language;
return R;
}
void stt_engine_free(SttEngine * eng) {
if (!eng) return;
delete eng;
}

103
dist/jni/stt_engine.h vendored Normal file
View File

@ -0,0 +1,103 @@
// Engine STT Whisper-Small Qualcomm AI Hub (HfWhisper KV-cache) in-process.
// Load une fois (encoder QAIRT context + decoder QAIRT context + mel_filters +
// vocab), transcribe N fois (PCM 16kHz mono -> texte FR/EN).
//
// Backend : ONNX Runtime + QNN ExecutionProvider (HTP V79 sur SM8750).
// Empreinte RAM : ~545 MB (encoder ctx 201 MB + decoder ctx 345 MB).
//
// Bench de référence prod (Whisper-Small, FR, audio 1.6s) :
// mel 189 ms (CPU C++) + encoder 125 ms (NPU) + decoder 510 ms (NPU, 22 tokens)
// total 825 ms, RTF 0.51, RAM peak 545 MB.
//
// Construit pour remplacer WhisperHybridEngine.kt + libmel_extractor.so + VadStage.kt
// derrière une API unifiée Kazeia-Engine. La sélection de backend (HTP vs CPU) est
// automatique via QNN EP : si le contexte QAIRT est trouvé, HTP ; sinon CPU fallback.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
struct SttEngine; // opaque
struct SttEngineLoadCfg {
// Dir contenant : HfWhisperEncoder.onnx + HfWhisperEncoder_qairt_context.bin,
// HfWhisperDecoder.onnx + HfWhisperDecoder_qairt_context.bin,
// mel_filters.json (ou .bin), vocab.json (BPE byte-level).
const char * model_dir = nullptr;
// 1 = QNN HTP (NPU, défaut prod). 0 = CPU fallback (ONNX Runtime CPU EP).
// QNN EP requiert que libQnnHtp.so + libQnnHtpV79Skel.so + libcdsprpc.so soient
// dans LD_LIBRARY_PATH (côté tablette : push depuis dist/lib/).
bool use_htp = true;
int n_threads = 6;
int max_decode_steps = 200; // borne hard du loop decoder autorégressif
};
struct SttTranscribeCfg {
// PCM mono 16-bit signed, n'importe quelle longueur (le mel pad fait à 30s).
// Si vous avez du PCM 24kHz, resamplez externe avant (ou utilisez SttTranscribeWaveform).
const int16_t * pcm16 = nullptr;
int n_samples = 0;
int sample_rate = 16000;
// "fr", "en", "auto", etc. Whisper-Small supporte 99 langues. "auto" = détection
// via la langue token de Whisper.
const char * language = "fr";
// 1 = force le mode "transcribe" (FR -> texte FR). 0 = laisse Whisper choisir
// entre transcribe et translate (translate force la sortie en EN, pas voulu pour
// Kazeia FR). Défaut : 1 (cf override prod KazeiaService).
bool force_transcribe = true;
// Inclure les timestamps (segments + offsets) dans la sortie.
bool with_timestamps = false;
};
struct SttSegment {
int t0_ms; // début segment (ms relatif au début audio)
int t1_ms; // fin segment
std::string text;
};
struct SttTranscribeResult {
int err = 0; // 0 = ok, <0 = err
std::string text; // texte concaténé (sans timestamps)
std::vector<SttSegment> segments; // vide si !with_timestamps
std::string detected_language; // "fr", "en", ...
// Métriques perf (ms)
int mel_ms = 0;
int encoder_ms = 0;
int decoder_ms = 0;
int total_ms = 0;
float rtf = 0.0f; // total_ms / (audio_duration_ms)
int n_tokens = 0;
};
// Charge encoder + decoder + mel basis + vocab. Retourne nullptr si échec.
// Idempotent vis-à-vis de llama_backend_init() (cohabite avec tts_engine).
SttEngine * stt_engine_load(const SttEngineLoadCfg & cfg);
// Transcrit une fenêtre audio. Stateful pour le warm path NPU (sessions ORT
// gardées en RAM entre appels).
SttTranscribeResult stt_engine_transcribe(SttEngine * eng, const SttTranscribeCfg & cfg);
// Libère tout (encoder ctx, decoder ctx, sessions ORT, vocab).
void stt_engine_free(SttEngine * eng);
// Helper VAD RMS énergie : renvoie true si le buffer PCM contient de la parole
// (énergie > seuil). Compatible drop-in avec VadStage.kt prod (frame=1600 @ 16kHz,
// seuil 150 PCM, ≥3 frames consécutives parole, ≥8 frames silence).
// Tu peux passer le PCM en streaming par chunks de 100ms et lire is_speech / is_end_of_speech.
struct SttVadState;
SttVadState * stt_vad_new(int sample_rate = 16000,
int frame_size = 1600,
int rms_threshold = 150,
int min_speech_frames = 3,
int silence_end_frames = 8);
bool stt_vad_push(SttVadState * s, const int16_t * pcm, int n_samples);
bool stt_vad_is_speech(const SttVadState * s);
bool stt_vad_is_end_of_speech(const SttVadState * s);
void stt_vad_reset(SttVadState * s);
void stt_vad_free(SttVadState * s);

113
dist/jni/stt_test.cpp vendored Normal file
View File

@ -0,0 +1,113 @@
// Test standalone S1.3 : exercer kazeia_mel (config_whisper) + VAD RMS.
// Usage : kazeia_stt_test <mel_filters.bin> <audio16k_mono.wav>
//
// Vérifie que le mel produit a la bonne shape [80 x 3000] et que les valeurs sont
// dans la plage Whisper (~[-1, 0.5] après norm). Permet de valider le mel partagé
// AVANT de brancher ORT/QNN en S2.
#include "kazeia_mel.h"
#include "stt_engine.h"
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <vector>
#include <fstream>
static std::vector<int16_t> read_wav_pcm16(const char* path, int & sr_out, int & channels_out) {
FILE* f = fopen(path, "rb");
if (!f) { fprintf(stderr, "open %s FAIL\n", path); return {}; }
char riff[12];
if (fread(riff, 1, 12, f) != 12 || memcmp(riff, "RIFF", 4) != 0 || memcmp(riff+8, "WAVE", 4) != 0) {
fprintf(stderr, "not RIFF/WAVE\n"); fclose(f); return {};
}
uint16_t channels = 0, bps = 0;
uint32_t sr = 0, data_sz = 0;
long data_off = -1;
while (!feof(f)) {
char cid[4]; uint32_t csz;
if (fread(cid, 1, 4, f) != 4) break;
if (fread(&csz, 4, 1, f) != 1) break;
if (memcmp(cid, "fmt ", 4) == 0) {
std::vector<uint8_t> buf(csz);
fread(buf.data(), 1, csz, f);
channels = *(uint16_t*)&buf[2];
sr = *(uint32_t*)&buf[4];
bps = *(uint16_t*)&buf[14];
} else if (memcmp(cid, "data", 4) == 0) {
data_sz = csz; data_off = ftell(f); break;
} else {
fseek(f, csz, SEEK_CUR);
}
}
if (data_off < 0 || channels != 1 || bps != 16) {
fprintf(stderr, "WAV mono 16-bit attendu (got ch=%d bps=%d)\n", channels, bps);
fclose(f); return {};
}
sr_out = (int)sr; channels_out = channels;
fseek(f, data_off, SEEK_SET);
std::vector<int16_t> pcm(data_sz / 2);
fread(pcm.data(), 2, pcm.size(), f);
fclose(f);
return pcm;
}
int main(int argc, char** argv) {
setvbuf(stderr, nullptr, _IONBF, 0);
setvbuf(stdout, nullptr, _IONBF, 0);
if (argc < 3) {
fprintf(stderr, "usage: %s <mel_filters.bin> <audio16k_mono.wav>\n", argv[0]);
return 1;
}
// 1) Mel basis 80x201 (Whisper)
auto cfg = kazeia_mel::config_whisper();
std::vector<float> mel_basis;
if (!kazeia_mel::load_mel_basis(argv[1], cfg, mel_basis)) return 2;
fprintf(stderr, "mel_basis Whisper [%d, %d] OK (%zu f32)\n",
cfg.n_mels, cfg.n_fft/2+1, mel_basis.size());
// 2) WAV
int sr = 0, ch = 0;
auto pcm = read_wav_pcm16(argv[2], sr, ch);
if (pcm.empty()) return 3;
fprintf(stderr, "wav : %zu samples @ %d Hz = %.2f s\n",
pcm.size(), sr, (double)pcm.size() / sr);
if (sr != 16000) fprintf(stderr, "WARNING: sr=%d != 16000 (resample externe requis)\n", sr);
// 3) Convert to f32
std::vector<float> wav(pcm.size());
for (size_t i = 0; i < pcm.size(); ++i) wav[i] = (float)pcm[i] / 32768.0f;
// 4) Mel via kazeia_mel
int T = 0;
auto mel = kazeia_mel::compute(wav, mel_basis, cfg, T);
fprintf(stderr, "mel computed : [%d mels, %d frames] = %zu f32 (cible 80 x 3000 Whisper)\n",
cfg.n_mels, T, mel.size());
float mn = 1e9f, mx = -1e9f; double sum = 0;
for (float v : mel) { if (v < mn) mn = v; if (v > mx) mx = v; sum += v; }
fprintf(stderr, "mel stats : min=%.4f max=%.4f mean=%.4f\n",
mn, mx, (float)(sum / mel.size()));
fprintf(stderr, "(plage Whisper attendue ~[-1.0, 0.5])\n");
// 5) VAD test : push tout le PCM par chunks de 1600 samples
fprintf(stderr, "\n=== VAD RMS test (frame=1600=100ms, seuil=150) ===\n");
SttVadState * vad = stt_vad_new(16000, 1600, 150, 3, 8);
int speech_seen = 0, eos_count = 0;
for (size_t off = 0; off + 1600 <= pcm.size(); off += 1600) {
stt_vad_push(vad, pcm.data() + off, 1600);
if (stt_vad_is_speech(vad)) speech_seen += 1;
if (stt_vad_is_end_of_speech(vad)) eos_count += 1;
}
fprintf(stderr, "VAD : %d frames de parole / %zu total, %d end-of-speech\n",
speech_seen, pcm.size() / 1600, eos_count);
stt_vad_free(vad);
// 6) Dump mel pour comparaison externe si KZSTT_DUMP_MEL=path
if (const char * p = std::getenv("KZSTT_DUMP_MEL")) {
std::ofstream f(p, std::ios::binary);
f.write((char*)mel.data(), mel.size() * sizeof(float));
fprintf(stderr, "mel dumped -> %s\n", p);
}
return 0;
}

View File

@ -19,6 +19,7 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <ctime>
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <vector> #include <vector>
@ -67,11 +68,16 @@ int main(int argc, char** argv) {
// l'utilise comme override pour ce synth (clonage vocal in-process). // l'utilise comme override pour ce synth (clonage vocal in-process).
std::vector<float> ref_xvec; std::vector<float> ref_xvec;
if (const char * ref_wav = getenv("KZTTS_REF_WAV")) { if (const char * ref_wav = getenv("KZTTS_REF_WAV")) {
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
ref_xvec = tts_engine_encode_speaker_wav(eng, ref_wav); ref_xvec = tts_engine_encode_speaker_wav(eng, ref_wav);
clock_gettime(CLOCK_MONOTONIC, &t1);
double enc_s = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) * 1e-9;
if (ref_xvec.empty()) { if (ref_xvec.empty()) {
fprintf(stderr, "tts_pipeline: KZTTS_REF_WAV=%s FAIL (encoder missing or read FAIL)\n", ref_wav); fprintf(stderr, "tts_pipeline: KZTTS_REF_WAV=%s FAIL (encoder missing or read FAIL)\n", ref_wav);
} else { } else {
fprintf(stderr, "tts_pipeline: KZTTS_REF_WAV=%s -> x_vector[%zu] OK\n", ref_wav, ref_xvec.size()); fprintf(stderr, "tts_pipeline: KZTTS_REF_WAV=%s -> x_vector[%zu] in %.3fs\n",
ref_wav, ref_xvec.size(), enc_s);
} }
} }

15
dist/lib/README_ORT.md vendored Normal file
View File

@ -0,0 +1,15 @@
# libonnxruntime.so (Microsoft ONNX Runtime + QNN EP)
Cette lib est requise par stt_engine.cpp pour exécuter Whisper-Small sur HTP V79.
Non versionnée dans git à cause de sa taille (20 MB).
Source : Maven `com.microsoft.onnxruntime:onnxruntime-android-qnn:1.24.3`.
Pour récupérer : extraire de l'AAR Gradle cache :
```bash
AAR=~/.gradle/caches/8.12/transforms/*/transformed/onnxruntime-android-qnn-1.24.3
cp $AAR/jni/arm64-v8a/libonnxruntime.so /opt/Kazeia-engine/dist/lib/
cp $AAR/headers/*.h /opt/Kazeia-engine/dist/include/onnxruntime/
```
Headers C++ : dans `dist/include/onnxruntime/` (versionnés, ~1 MB).