Skip to content

Surface ys_log_probs on OfflineRecognizerResult (Kotlin JNI) - #3638

Open
ayeats wants to merge 1 commit into
k2-fsa:masterfrom
ayeats:expose-ys-log-probs-offline
Open

ayeats wants to merge 1 commit into
k2-fsa:masterfrom
ayeats:expose-ys-log-probs-offline

Conversation

@ayeats

@ayeats ayeats commented May 26, 2026

Copy link
Copy Markdown

Summary

  • Surfaces per-token log-probabilities (OfflineRecognitionResult::ys_log_probs) through the offline JNI to the Kotlin OfflineRecognizerResult data class as ysLogProbs: FloatArray.
  • Mirrors PR Expose ys probs to JNI, Kotlin and Java API #2736, which did the same for OnlineRecognizerResult (streaming). The offline path is the missing half.
  • The C++ side already computes and populates this field in the transducer greedy decoder (see csrc/offline-transducer-greedy-search-decoder.cc, lines 68-76); the binding was simply dropping it.

Why

With the field exposed, downstream callers can compute a real per-token confidence on-device:

  • geo_mean = exp(mean(log_probs)) — joint probability proxy
  • min = exp(min(log_probs)) — weakest-link signal

Without it, applications fall back to heuristic proxies (tokens-per-second, timestamp variance) that saturate near 1.0 even on mishearings.

Concrete example from a smoke test with Parakeet TDT 0.6B-v2 on a Qualcomm QCS6490 device (Android arm64-v8a):

utterance mean exp(log_p) min exp(log_p) notes
Call James Smith (clean) 0.96 0.71 clean
Call the on-call anesthesiologist (clean) 0.97 0.81 clean
Call Health Supervisor (House mis-heard) 0.91 0.35 mean hides it; min flags it
Col Cath Lad (catastrophic mis-hear) 0.68 0.33 both flag it

The geo-mean alone misses single-token uncertainty; the per-token array unlocks a min-based gate. That's only possible if the values reach Kotlin.

What changed

  • sherpa-onnx/jni/offline-recognizer.cc
    • GetMethodID signature extended with one extra [F.
    • NewFloatArray + SetFloatArrayRegion for result.ys_log_probs.
    • Passed into the Kotlin constructor; DeleteLocalRef on cleanup.
  • sherpa-onnx/kotlin-api/OfflineRecognizer.kt
    • val ysLogProbs: FloatArray appended to OfflineRecognizerResult.

The field is empty (0-length jfloatArray) for non-transducer / non-greedy decoders until the corresponding C++ paths are extended. Documented in the Kotlin comment.

Compatibility

  • C++ behaviour unchanged. result.ys_log_probs was already being computed; only the JNI pass-through is new.
  • Kotlin data class gains a new field. Source-compatible for any code that reads existing fields (text, tokens, timestamps, etc.); anyone constructing OfflineRecognizerResult manually (rare — usually constructed by the JNI) will need to add the field. Symmetric to PR Expose ys probs to JNI, Kotlin and Java API #2736's change to OnlineRecognizerResult.

Test plan

  • Builds clean as a local AAR via build-android-arm64-v8a.sh on Android NDK 27.
  • Smoke-tested on a Qualcomm QCS6490 Android 13 device with Parakeet TDT 0.6B-v2 INT8: ysLogProbs.size == tokens.size, values are negative log-probs as expected, exp(values) lies in (0, 1].
  • CI build matrix.

Related

Summary by CodeRabbit

  • New Features
    • Recognition results from offline recognizers now include per-token log-probabilities, enabling detailed confidence analysis at the token level. This feature is populated for offline transducer greedy decoder models.

Review Change Stack

The offline transducer greedy decoder already populates
OfflineRecognitionResult::ys_log_probs (see
csrc/offline-transducer-greedy-search-decoder.cc, lines 68-76),
but the JNI binding dropped the field on the floor, so downstream
Kotlin / Java callers can't compute a real per-token confidence
on-device.

This patch mirrors PR k2-fsa#2736 (which surfaced ysProbs on
OnlineRecognizerResult for streaming) for the offline path:

  - jni/offline-recognizer.cc: extend the GetMethodID signature with
    an extra '[F', allocate NewFloatArray + SetFloatArrayRegion for
    result.ys_log_probs, pass into the Kotlin data-class constructor,
    DeleteLocalRef afterwards.
  - kotlin-api/OfflineRecognizer.kt: append val ysLogProbs: FloatArray
    to OfflineRecognizerResult.

The field is empty for non-greedy / non-transducer decoders until
the corresponding C++ paths are extended; the empty array is the
documented fallback.

Tested with Parakeet TDT 0.6B-v2 on an Android arm64-v8a Versity
9740 (Qualcomm QCS6490). Mean exp(log_prob) tracks engine
confidence and reveals weak tokens that mean-only metrics hide
(observed: "Call the House Supervisor" mis-heard as "Call
Health Supervisor" with mean 0.91 but per-token min 0.35).
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label May 26, 2026
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The pull request exposes per-token log-probabilities from the offline recognizer to Java/Kotlin clients. A new ysLogProbs field is added to the OfflineRecognizerResult data class, and the JNI glue layer is updated to convert and pass the native log-probabilities array when constructing the result object.

Changes

Per-token log-probabilities in offline recognizer result

Layer / File(s) Summary
Kotlin data class field addition
sherpa-onnx/kotlin-api/OfflineRecognizer.kt
OfflineRecognizerResult gains a new ysLogProbs: FloatArray field with inline documentation stating it is populated only by the offline transducer greedy decoder.
JNI native binding for log-probabilities
sherpa-onnx/jni/offline-recognizer.cc
The JNI method descriptor is updated to include the new parameter. A float array is allocated from result.ys_log_probs, passed to the result constructor, and the local reference is deleted after use.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • k2-fsa/sherpa-onnx#2843: Adds and populates the ys_log_probs field in the offline recognition result C API that this PR exposes through JNI/Kotlin.
  • k2-fsa/sherpa-onnx#2846: Populates per-token log probabilities into r.ys_log_probs in the decoder internals that this PR wires through the language binding.
  • k2-fsa/sherpa-onnx#3553: Adds ys_log_probs exposure through the Go binding in a parallel effort to expose the same native field through a different language API.

Suggested reviewers

  • csukuangfj

Poem

🐰 A token speaks: "I'm here now, log-prob and all!"
Through JNI's bridge, the probabilities call.
Kotlin hears the whisper, floats flow so free,
From native to Java—recognition's spree! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: surfacing per-token log-probabilities (ys_log_probs) on the OfflineRecognizerResult Kotlin class through JNI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the JNI and Kotlin APIs for the offline recognizer to expose per-token log-probabilities (ys_log_probs / ysLogProbs). This allows callers to implement confidence-based policies. A review comment points out a potential issue where calling SetFloatArrayRegion with a nullptr data pointer (when ys_log_probs is empty) can trigger JNI warnings or undefined behavior, suggesting a guard check to ensure the vector is not empty before copying.

Comment on lines +676 to +678
jfloatArray jys_log_probs = env->NewFloatArray(result.ys_log_probs.size());
env->SetFloatArrayRegion(jys_log_probs, 0, result.ys_log_probs.size(),
result.ys_log_probs.data());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When result.ys_log_probs is empty (which is the default for non-transducer or non-greedy decoders), result.ys_log_probs.data() may return nullptr. Calling SetFloatArrayRegion with a nullptr buffer, even with a size of 0, can trigger JNI warnings (especially with -Xcheck:jni enabled) or lead to undefined behavior/crashes on some JVM implementations.

It is safer to guard the SetFloatArrayRegion call with a check to ensure the vector is not empty.

  jfloatArray jys_log_probs = env->NewFloatArray(result.ys_log_probs.size());
  if (!result.ys_log_probs.empty()) {
    env->SetFloatArrayRegion(jys_log_probs, 0, result.ys_log_probs.size(),
                             result.ys_log_probs.data());
  }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
sherpa-onnx/kotlin-api/OfflineRecognizer.kt (1)

16-19: ⚡ Quick win

Default ysLogProbs to an empty array.

This is a public data class, so making the new field mandatory forces Kotlin call sites that construct OfflineRecognizerResult manually to change even though the documented fallback is already “empty”. A default here keeps those source call sites working without changing the JNI path.

Proposed change
-    val ysLogProbs: FloatArray,
+    val ysLogProbs: FloatArray = floatArrayOf(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/kotlin-api/OfflineRecognizer.kt` around lines 16 - 19, Make the
public data class OfflineRecognizerResult's new field ysLogProbs optional by
giving it a default empty FloatArray so existing Kotlin call sites don't break;
specifically update the declaration of val ysLogProbs in OfflineRecognizerResult
to have a default (e.g., FloatArray(0) or floatArrayOf()) so callers can omit
the parameter while JNI consumers still receive an empty array when not
populated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sherpa-onnx/jni/offline-recognizer.cc`:
- Around line 673-682: Check that result.ys_log_probs has the same length as the
token sequence before exposing it to Kotlin: if result.ys_log_probs.size() !=
result.tokens.size() (or is zero) then create an empty jfloatArray (length 0)
and use that instead of blindly creating jys_log_probs from result.ys_log_probs;
otherwise create jys_log_probs with env->NewFloatArray and
env->SetFloatArrayRegion as currently done and pass it into NewObject (jresult)
alongside jtext, jtokens, jtimestamps, jlang, jemotion, jevent, jdurations so
the JNI path mirrors the C API guard.

---

Nitpick comments:
In `@sherpa-onnx/kotlin-api/OfflineRecognizer.kt`:
- Around line 16-19: Make the public data class OfflineRecognizerResult's new
field ysLogProbs optional by giving it a default empty FloatArray so existing
Kotlin call sites don't break; specifically update the declaration of val
ysLogProbs in OfflineRecognizerResult to have a default (e.g., FloatArray(0) or
floatArrayOf()) so callers can omit the parameter while JNI consumers still
receive an empty array when not populated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 798d29aa-54fa-4964-b7c8-49249f277134

📥 Commits

Reviewing files that changed from the base of the PR and between a703cf6 and 606b73e.

📒 Files selected for processing (2)
  • sherpa-onnx/jni/offline-recognizer.cc
  • sherpa-onnx/kotlin-api/OfflineRecognizer.kt

Comment on lines +673 to +682
// Per-token log-probabilities. The C++ greedy decoder populates
// result.ys_log_probs; surface them to Kotlin so callers can apply
// a real confidence-based policy instead of a heuristic proxy.
jfloatArray jys_log_probs = env->NewFloatArray(result.ys_log_probs.size());
env->SetFloatArrayRegion(jys_log_probs, 0, result.ys_log_probs.size(),
result.ys_log_probs.data());

jobject jresult = env->NewObject(cls, ctor, jtext, jtokens, jtimestamps,
jlang, jemotion, jevent, jdurations);
jlang, jemotion, jevent, jdurations,
jys_log_probs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only expose ys_log_probs when it matches the token count.

The C API already guards this field behind a length check, but this JNI path forwards any non-empty result.ys_log_probs. If native code ever returns a partial/misaligned vector, Kotlin callers will see “per-token” confidences that no longer line up with tokens. Please mirror the existing guard here and fall back to an empty array on mismatch.

Proposed change
-  jfloatArray jys_log_probs = env->NewFloatArray(result.ys_log_probs.size());
-  env->SetFloatArrayRegion(jys_log_probs, 0, result.ys_log_probs.size(),
-                           result.ys_log_probs.data());
+  const jsize ys_log_probs_size =
+      result.ys_log_probs.size() == result.tokens.size()
+          ? static_cast<jsize>(result.ys_log_probs.size())
+          : 0;
+  jfloatArray jys_log_probs = env->NewFloatArray(ys_log_probs_size);
+  if (ys_log_probs_size > 0) {
+    env->SetFloatArrayRegion(jys_log_probs, 0, ys_log_probs_size,
+                             result.ys_log_probs.data());
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sherpa-onnx/jni/offline-recognizer.cc` around lines 673 - 682, Check that
result.ys_log_probs has the same length as the token sequence before exposing it
to Kotlin: if result.ys_log_probs.size() != result.tokens.size() (or is zero)
then create an empty jfloatArray (length 0) and use that instead of blindly
creating jys_log_probs from result.ys_log_probs; otherwise create jys_log_probs
with env->NewFloatArray and env->SetFloatArrayRegion as currently done and pass
it into NewObject (jresult) alongside jtext, jtokens, jtimestamps, jlang,
jemotion, jevent, jdurations so the JNI path mirrors the C API guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant