Skip to content

feat: add SetOption/GetOption CXX wrapper - #3309

Merged
csukuangfj merged 1 commit into
k2-fsa:masterfrom
ZhaoChaoqun:feat/setoption-cxx-wrapper
Mar 18, 2026
Merged

csukuangfj merged 1 commit into
k2-fsa:masterfrom
ZhaoChaoqun:feat/setoption-cxx-wrapper

Conversation

@ZhaoChaoqun

@ZhaoChaoqun ZhaoChaoqun commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ref #3101 (Part 1c) — depends on #3308

Add SetOption/GetOption methods to the C++ wrapper (cxx-api) for both OnlineStream and OfflineStream.

Files Changed (2 files, +22 lines)

File Change
cxx-api.h Add method declarations
cxx-api.cc Implementations calling C API

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added per-stream configuration options: set and retrieve custom options for both online and offline streams.
    • Added final chunk signaling for online streams: enables explicit notification when processing the final audio chunk in streaming scenarios.
    • Extended API support across C, C++, and Python interfaces for unified stream control and option management.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 12, 2026
@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes extend sherpa-onnx's C and C++ APIs to support per-stream runtime options and final-chunk signaling for Paraformer streaming. New functions enable getting/setting options on both online and offline streams with underlying storage via unordered maps. Paraformer decoding logic is enhanced to handle final chunks through modified readiness checks, adjusted chunk sizing, and CIF tail-flush operations.

Changes

Cohort / File(s) Summary
C API Headers & Declarations
sherpa-onnx/c-api/c-api.h, sherpa-onnx/c-api/c-api.cc
Added 5 new public C API functions: SherpaOnnxOnlineStreamSetFinalChunk, SherpaOnnxOnlineStreamSetOption/GetOption, and SherpaOnnxOfflineStreamSetOption/GetOption. These delegate to underlying C++ stream implementations for option management and final-chunk marking.
C++ Wrapper Layer
sherpa-onnx/c-api/cxx-api.h, sherpa-onnx/c-api/cxx-api.cc
Added SetOption/GetOption const methods to both OnlineStream and OfflineStream classes, delegating to corresponding C API functions.
Symbol Exports
sherpa-onnx/c-api/sherpa-onnx-symbols-c.exp
Added 5 new exported symbols for the newly introduced C API functions.
OnlineStream Core
sherpa-onnx/csrc/online-stream.h, sherpa-onnx/csrc/online-stream.cc
Introduced per-stream option storage map and Paraformer final-chunk flag with public accessors: SetParaformerFinalChunk, IsParaformerFinalChunk, and option management methods (SetOption, GetOption, GetOptionInt, GetOptionFloat, HasOption). Reset() clears the final-chunk flag.
OfflineStream Core
sherpa-onnx/csrc/offline-stream.h, sherpa-onnx/csrc/offline-stream.cc
Introduced per-stream option storage map with public accessors: SetOption, GetOption, GetOptionInt, GetOptionFloat, HasOption. Added unordered_map header dependency.
Paraformer Decoder Logic
sherpa-onnx/csrc/online-recognizer-paraformer-impl.h
Modified final-chunk handling: IsReady now allows processing short final chunks, DecodeStream adjusts chunk sizes and frame advancement for final chunks, CIF tail-flush logic added to output residual tokens when final chunk threshold is met, token decoding now skips tokens 0/1/2 instead of just 0.
Python Bindings
sherpa-onnx/python/csrc/online-stream.cc
Added Python binding for set_paraformer_final_chunk method with default argument and GIL release guard.

Sequence Diagram

sequenceDiagram
    participant Client
    participant OnlineStream
    participant Impl
    participant Paraformer
    
    Client->>OnlineStream: SetParaformerFinalChunk(true)
    OnlineStream->>Impl: SetParaformerFinalChunk(true)
    Impl->>Impl: paraformer_is_final_ = true
    
    Client->>OnlineStream: DecodeStream()
    OnlineStream->>Paraformer: IsReady()?
    Paraformer->>OnlineStream: Check IsParaformerFinalChunk()
    OnlineStream-->>Paraformer: true (final chunk mode)
    Paraformer-->>OnlineStream: true (ready despite < chunk_size)
    
    OnlineStream->>Paraformer: DecodeStream(final_chunk=true)
    Paraformer->>Paraformer: Adjust chunk_size for remaining frames
    Paraformer->>Paraformer: CIF processing with tail-flush check
    
    alt Final chunk && integrate >= threshold
        Paraformer->>Paraformer: Flush residual token
        Paraformer->>Paraformer: Reset integrate & hidden state
    end
    
    Paraformer->>Paraformer: Skip special tokens (0,1,2)
    Paraformer-->>OnlineStream: Decoded tokens
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

size:S

Suggested reviewers

  • csukuangfj

Poem

🐰 Stream options now flow free,
Final chunks marked with glee,
Paraformer's tail does flush,
Tokens processed in a rush!
Per-stream config, C and C++ unified,
Speech recognition amplified! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 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: adding SetOption/GetOption methods to the C++ wrapper (CXX API) for stream objects.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan for PR comments
  • Generate coding plan

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

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility and robustness of stream handling in the sherpa-onnx library. By introducing a generic option mechanism, it allows for dynamic configuration of both online and offline streams. Furthermore, it specifically addresses challenges in streaming Paraformer models by improving final chunk processing, leading to more accurate and complete transcriptions, especially for short or ending utterances.

Highlights

  • C++ Wrapper Enhancements: Introduced generic SetOption and GetOption methods to both OnlineStream and OfflineStream classes within the C++ API, allowing for flexible runtime parameter configuration using key-value pairs.
  • Paraformer Streaming Improvements: Added SetParaformerFinalChunk functionality to OnlineStream to signal the final audio chunk for streaming Paraformer models. This enables short chunk acceptance, zero-padding for incomplete chunks, and a CIF tail token flush mechanism to prevent missed tokens at the end of an utterance.
  • C API Exposure: Exposed the new SetOption, GetOption, and SetParaformerFinalChunk functionalities through the C API, ensuring broader accessibility and interoperability.
Changelog
  • sherpa-onnx/c-api/c-api.cc
    • Added SherpaOnnxOnlineStreamSetFinalChunk function.
    • Added SherpaOnnxOnlineStreamSetOption and SherpaOnnxOnlineStreamGetOption functions.
    • Added SherpaOnnxOfflineStreamSetOption and SherpaOnnxOfflineStreamGetOption functions.
  • sherpa-onnx/c-api/c-api.h
    • Declared SherpaOnnxOnlineStreamSetFinalChunk with documentation for Paraformer final chunk handling.
    • Declared SherpaOnnxOnlineStreamSetOption and SherpaOnnxOnlineStreamGetOption with documentation for generic option management.
    • Declared SherpaOnnxOfflineStreamSetOption and SherpaOnnxOfflineStreamGetOption with documentation for generic option management.
  • sherpa-onnx/c-api/cxx-api.cc
    • Implemented SetOption and GetOption methods for OnlineStream by calling the C API.
    • Implemented SetOption and GetOption methods for OfflineStream by calling the C API.
  • sherpa-onnx/c-api/cxx-api.h
    • Declared SetOption and GetOption methods within the OnlineStream class.
    • Declared SetOption and GetOption methods within the OfflineStream class.
  • sherpa-onnx/c-api/sherpa-onnx-symbols-c.exp
    • Exported new C API symbols: _SherpaOnnxOfflineStreamGetOption, _SherpaOnnxOfflineStreamSetOption, _SherpaOnnxOnlineStreamGetOption, _SherpaOnnxOnlineStreamInputFinished, _SherpaOnnxOnlineStreamSetFinalChunk, _SherpaOnnxOnlineStreamSetOption.
  • sherpa-onnx/csrc/offline-stream.cc
    • Included <unordered_map> for option storage.
    • Implemented SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods within OfflineStream::Impl.
    • Added an unordered_map named options_ to OfflineStream::Impl for storing key-value options.
    • Implemented SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods for the OfflineStream class, delegating to its Impl.
  • sherpa-onnx/csrc/offline-stream.h
    • Declared SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods in the OfflineStream class.
  • sherpa-onnx/csrc/online-recognizer-paraformer-impl.h
    • Modified IsReady to accept short chunks when IsParaformerFinalChunk() is true.
    • Adjusted DecodeStream to handle short chunks by padding with zeros if IsParaformerFinalChunk() is true.
    • Implemented CIF tail token flush logic in DecodeStream when IsParaformerFinalChunk() is true and integrate meets a minimum alpha threshold.
    • Updated token skipping logic to include sos(1) and eos(2) tokens.
    • Added kCifTailFlushMinAlpha constant for CIF tail flushing.
  • sherpa-onnx/csrc/online-stream.cc
    • Included <string> and <unordered_map> for option storage.
    • Initialized paraformer_is_final_ to false during stream reset.
    • Implemented SetParaformerFinalChunk, IsParaformerFinalChunk, SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods within OnlineStream::Impl.
    • Added paraformer_is_final_ boolean and an unordered_map named options_ to OnlineStream::Impl.
    • Implemented SetParaformerFinalChunk, IsParaformerFinalChunk, SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods for the OnlineStream class, delegating to its Impl.
  • sherpa-onnx/csrc/online-stream.h
    • Included <string> for option handling.
    • Declared SetParaformerFinalChunk, IsParaformerFinalChunk, SetOption, HasOption, GetOption, GetOptionInt, and GetOptionFloat methods in the OnlineStream class.
  • sherpa-onnx/python/csrc/online-stream.cc
    • Exposed set_paraformer_final_chunk method to the Python binding for OnlineStream.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@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 introduces SetOption/GetOption methods for OnlineStream and OfflineStream in the C++ wrapper, along with the underlying C-API and implementation changes. It also adds logic to handle final chunks in the streaming Paraformer model.

The changes are generally good, but I have identified a few areas for improvement:

  • There is a critical issue where std::stoi and std::stof are used without exception handling, which could lead to crashes if an option has an invalid format.
  • There is some implementation inconsistency where a specific paraformer_is_final_ flag is introduced alongside the generic options_ map, while the documentation suggests using the generic mechanism.
  • The option handling logic (GetOptionInt, GetOptionFloat, etc.) is duplicated between OnlineStream::Impl and OfflineStream::Impl. This duplicated utility code should be moved to a common file to improve reusability and maintainability, aligning with repository rules.

Comment on lines +243 to +257
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The std::stoi and std::stof functions will throw an exception (e.g., std::invalid_argument or std::out_of_range) if the string value from the options map cannot be converted to a number. This unhandled exception will cause the program to crash.

Please add try-catch blocks to handle potential conversion errors gracefully and return the default value.

  int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
      try {
        return std::stoi(it->second);
      } catch (const std::exception &) {
        // You may want to log a warning here
        return default_value;
      }
    }
    return default_value;
  }

  float GetOptionFloat(const std::string &key, float default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
      try {
        return std::stof(it->second);
      } catch (const std::exception &) {
        // You may want to log a warning here
        return default_value;
      }
    }
    return default_value;
  }

Comment on lines +159 to +173
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The std::stoi and std::stof functions will throw an exception (e.g., std::invalid_argument or std::out_of_range) if the string value from the options map cannot be converted to a number. This unhandled exception will cause the program to crash.

Please add try-catch blocks to handle potential conversion errors gracefully and return the default value.

  int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
      try {
        return std::stoi(it->second);
      } catch (const std::exception &) {
        // You may want to log a warning here
        return default_value;
      }
    }
    return default_value;
  }

  float GetOptionFloat(const std::string &key, float default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
      try {
        return std::stof(it->second);
      } catch (const std::exception &) {
        // You may want to log a warning here
        return default_value;
      }
    }
    return default_value;
  }

Comment thread sherpa-onnx/csrc/online-stream.cc Outdated
Comment on lines +206 to +207
bool paraformer_is_final_ = false;
std::unordered_map<std::string, std::string> options_;

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

The paraformer_is_final_ flag seems redundant now that a generic options_ map is available. This is also inconsistent with the documentation for SetOption which uses "is_final" as an example.

To improve consistency and avoid redundancy, I suggest removing paraformer_is_final_ and implementing SetParaformerFinalChunk() and IsParaformerFinalChunk() using the options_ map.

For example:
// In OnlineStream::Impl
void SetParaformerFinalChunk(bool is_final) {
options_["is_final"] = is_final ? "true" : "false";
}

bool IsParaformerFinalChunk() const {
auto it = options_.find("is_final");
if (it != options_.end()) {
return it->second == "true";
}
return false;
}
This change would make the implementation consistent with the documentation and unify the two mechanisms for setting stream options.

Comment on lines +243 to +257
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}

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

The option handling logic, specifically GetOptionInt and GetOptionFloat methods, is duplicated between OfflineStream::Impl and OnlineStream::Impl. To adhere to repository guidelines and improve code reusability and maintainability, these utility functions should be extracted into a common utility file (e.g., options-utils.h and options-utils.cc) and reused by both implementations.

References
  1. Move duplicated utility functions, such as Trim, to a common utility file (e.g., text-utils.h and text-utils.cc) for reuse across the codebase.

@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: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sherpa-onnx/c-api/c-api.cc`:
- Around line 359-367: Add null-pointer checks and avoid returning pointers to
temporaries: in SherpaOnnxOnlineStreamSetOption and
SherpaOnnxOnlineStreamGetOption validate that stream is non-null and treat
key/value as empty strings when NULL (e.g., std::string k = key ? key : "";
std::string v = value ? value : "";), call stream->impl->SetOption(k, v) for
SetOption, and for GetOption call std::string out = stream->impl->GetOption(k)
and return a heap-allocated copy (e.g., strdup(out.c_str()) or the project's
string-copy helper) instead of returning out.c_str() from the temporary; apply
the same defensive null-check and return-copy pattern to
SherpaOnnxOfflineStreamSetOption and SherpaOnnxOfflineStreamGetOption.
- Around line 364-367: SherpaOnnxOnlineStreamGetOption currently returns
stream->impl->GetOption(key).c_str(), which can dangle when the stream is
destroyed or when SetOption mutates the underlying map; change it to return an
allocated copy (like strdup/malloc + memcpy) of the string so the C API owns
stable storage (matching patterns such as
SherpaOnnxGetOnlineStreamResultAsJson), and update the header comment for
SherpaOnnxOnlineStreamGetOption to document that the caller is responsible for
freeing the returned char* (or alternatively document lifetime semantics if you
choose to return an internal pointer instead). Ensure you reference
SherpaOnnxOnlineStreamGetOption, the impl::GetOption method, and SetOption when
making the change so the allocation and lifetime contract are consistent across
the C API.

In `@sherpa-onnx/c-api/c-api.h`:
- Around line 394-402: The doc comment for SherpaOnnxOnlineStreamSetOption
incorrectly uses "is_final" as an example (which Paraformer reads from a
dedicated flag, not this option map); update the example to a valid generic
per-stream option (e.g., "language" or "context" or another real option your
recognizer actually reads) or remove the specific example entirely so it does
not imply calling SherpaOnnxOnlineStreamSetOption will trigger final-chunk
behavior; keep references to SherpaOnnxOnlineStreamSetOption and
SherpaOnnxCreateOnlineStream so readers can locate the API.

In `@sherpa-onnx/c-api/cxx-api.h`:
- Around line 182-183: Change the API to avoid returning borrowed C strings and
to use std::string parameters: update SetOption(const char *key, const char
*value) to SetOption(const std::string &key, const std::string &value) and
change GetOption(const char *key) const to return std::string (not const char*)
so the caller owns the returned data; apply the same changes for the
OfflineStream variants (the other SetOption/GetOption declarations) and ensure
implementations copy/construct std::string from internal storage rather than
returning pointers into internal containers.

In `@sherpa-onnx/csrc/offline-stream.cc`:
- Around line 243-257: GetOptionInt and GetOptionFloat can throw from
std::stoi/std::stof and must not let exceptions escape into C callers; wrap the
calls to std::stoi and std::stof in try/catch blocks inside GetOptionInt and
GetOptionFloat, catch std::invalid_argument and std::out_of_range (optionally
std::exception as a fallback), and return default_value when parsing fails; keep
the same lookup via options_ and only attempt parsing if the key exists,
otherwise return default_value as before.

In `@sherpa-onnx/csrc/online-recognizer-paraformer-impl.h`:
- Around line 240-246: The current branch advances GetNumProcessedFrames() by
actual_chunk_size whenever s->IsParaformerFinalChunk(), which erroneously
removes the 1-frame overlap for final-chunk decodes that are actually full-size;
change the logic in online-recognizer-paraformer-impl.h so that
GetNumProcessedFrames() is increased by actual_chunk_size only when the final
chunk is truly short (actual_chunk_size < chunk_size_), otherwise continue to
advance by chunk_size_ - 1; update the branch around
s->IsParaformerFinalChunk(), actual_chunk_size, chunk_size_, and
GetNumProcessedFrames() to reflect this conditional behavior.

In `@sherpa-onnx/csrc/online-stream.cc`:
- Around line 159-172: GetOptionInt and GetOptionFloat currently call
std::stoi/std::stof directly and will throw exceptions for malformed or
out-of-range strings, so wrap the conversions in try/catch blocks that catch
std::invalid_argument and std::out_of_range (or std::exception) and return
default_value on error; specifically, in GetOptionInt(const std::string &key,
int32_t default_value) and GetOptionFloat(const std::string &key, float
default_value) check options_.find(key) as before, then attempt the
std::stoi/std::stof call inside a try block and return the parsed value on
success, but return default_value in the catch handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8737847-a1a3-4750-bb06-473bce0b62de

📥 Commits

Reviewing files that changed from the base of the PR and between 9b5bb2d and fc0be49.

📒 Files selected for processing (11)
  • sherpa-onnx/c-api/c-api.cc
  • sherpa-onnx/c-api/c-api.h
  • sherpa-onnx/c-api/cxx-api.cc
  • sherpa-onnx/c-api/cxx-api.h
  • sherpa-onnx/c-api/sherpa-onnx-symbols-c.exp
  • sherpa-onnx/csrc/offline-stream.cc
  • sherpa-onnx/csrc/offline-stream.h
  • sherpa-onnx/csrc/online-recognizer-paraformer-impl.h
  • sherpa-onnx/csrc/online-stream.cc
  • sherpa-onnx/csrc/online-stream.h
  • sherpa-onnx/python/csrc/online-stream.cc

Comment on lines +359 to +367
void SherpaOnnxOnlineStreamSetOption(const SherpaOnnxOnlineStream *stream,
const char *key, const char *value) {
stream->impl->SetOption(key, value);
}

const char *SherpaOnnxOnlineStreamGetOption(
const SherpaOnnxOnlineStream *stream, const char *key) {
return stream->impl->GetOption(key).c_str();
}

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 | 🟡 Minor

Missing null-pointer checks for string parameters.

The SetOption and GetOption functions pass key and value directly to std::string constructors. If a C caller passes NULL, this is undefined behavior and will likely crash.

Consider adding null checks consistent with other functions in this file:

🛡️ Proposed fix for null safety
 void SherpaOnnxOnlineStreamSetOption(const SherpaOnnxOnlineStream *stream,
                                      const char *key, const char *value) {
+  if (!stream || !key || !value) {
+    return;
+  }
   stream->impl->SetOption(key, value);
 }

 const char *SherpaOnnxOnlineStreamGetOption(
     const SherpaOnnxOnlineStream *stream, const char *key) {
+  if (!stream || !key) {
+    return "";
+  }
   return stream->impl->GetOption(key).c_str();
 }

Apply the same pattern to SherpaOnnxOfflineStreamSetOption and SherpaOnnxOfflineStreamGetOption.

Also applies to: 671-679

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/c-api/c-api.cc` around lines 359 - 367, Add null-pointer checks
and avoid returning pointers to temporaries: in SherpaOnnxOnlineStreamSetOption
and SherpaOnnxOnlineStreamGetOption validate that stream is non-null and treat
key/value as empty strings when NULL (e.g., std::string k = key ? key : "";
std::string v = value ? value : "";), call stream->impl->SetOption(k, v) for
SetOption, and for GetOption call std::string out = stream->impl->GetOption(k)
and return a heap-allocated copy (e.g., strdup(out.c_str()) or the project's
string-copy helper) instead of returning out.c_str() from the temporary; apply
the same defensive null-check and return-copy pattern to
SherpaOnnxOfflineStreamSetOption and SherpaOnnxOfflineStreamGetOption.

Comment on lines +364 to +367
const char *SherpaOnnxOnlineStreamGetOption(
const SherpaOnnxOnlineStream *stream, const char *key) {
return stream->impl->GetOption(key).c_str();
}

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 | 🟡 Minor

Returning pointer to internal storage may cause dangling pointer issues.

The GetOption functions return .c_str() on a reference to internal string storage. While this works because the underlying GetOption returns a reference to either a map element or a static empty string, the pointer becomes invalid if:

  1. The stream is destroyed
  2. The option is modified via SetOption with the same key

This differs from other C API patterns in this file (e.g., SherpaOnnxGetOnlineStreamResultAsJson) which allocate copies. Consider documenting the lifetime semantics in the header, or allocating a copy for consistency with the rest of the API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/c-api/c-api.cc` around lines 364 - 367,
SherpaOnnxOnlineStreamGetOption currently returns
stream->impl->GetOption(key).c_str(), which can dangle when the stream is
destroyed or when SetOption mutates the underlying map; change it to return an
allocated copy (like strdup/malloc + memcpy) of the string so the C API owns
stable storage (matching patterns such as
SherpaOnnxGetOnlineStreamResultAsJson), and update the header comment for
SherpaOnnxOnlineStreamGetOption to document that the caller is responsible for
freeing the returned char* (or alternatively document lifetime semantics if you
choose to return an internal pointer instead). Ensure you reference
SherpaOnnxOnlineStreamGetOption, the impl::GetOption method, and SetOption when
making the change so the allocation and lifetime contract are consistent across
the C API.

Comment thread sherpa-onnx/c-api/c-api.h
Comment on lines +394 to +402
/// Set a key-value option on an online stream.
/// This provides a generic mechanism for passing per-stream runtime parameters
/// to the recognizer (e.g., "is_final" for streaming Paraformer).
///
/// @param stream A pointer returned by SherpaOnnxCreateOnlineStream()
/// @param key The option name (e.g., "is_final")
/// @param value The option value (e.g., "true")
SHERPA_ONNX_API void SherpaOnnxOnlineStreamSetOption(
const SherpaOnnxOnlineStream *stream, const char *key, const char *value);

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 | 🟡 Minor

Don’t use "is_final" as the example option here.

The Paraformer path reads the dedicated final-chunk flag, not the generic option map, so this example suggests an API call that will not trigger the behavior described above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/c-api/c-api.h` around lines 394 - 402, The doc comment for
SherpaOnnxOnlineStreamSetOption incorrectly uses "is_final" as an example (which
Paraformer reads from a dedicated flag, not this option map); update the example
to a valid generic per-stream option (e.g., "language" or "context" or another
real option your recognizer actually reads) or remove the specific example
entirely so it does not imply calling SherpaOnnxOnlineStreamSetOption will
trigger final-chunk behavior; keep references to SherpaOnnxOnlineStreamSetOption
and SherpaOnnxCreateOnlineStream so readers can locate the API.

Comment on lines +182 to +183
void SetOption(const char *key, const char *value) const;
const char *GetOption(const char *key) const;

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare text-returning APIs in the C++ wrapper.
rg -n 'const char \*GetOption|std::string ' sherpa-onnx/c-api/cxx-api.h

Repository: k2-fsa/sherpa-onnx

Length of output: 5348


🏁 Script executed:

#!/bin/bash
# Find the implementation file for cxx-api.h
fd -t f 'cxx-api\.(cc|cpp)' sherpa-onnx/

# Also check what GetOption and SetOption do
grep -n "GetOption\|SetOption" sherpa-onnx/c-api/cxx-api.h | head -20

Repository: k2-fsa/sherpa-onnx

Length of output: 322


🏁 Script executed:

#!/bin/bash
# Let's check the implementation to understand the lifetime concern
# First find the implementation file
find sherpa-onnx -name "*.cc" -o -name "*.cpp" | xargs grep -l "GetOption" 2>/dev/null | head -5

Repository: k2-fsa/sherpa-onnx

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Check what the C API returns (the underlying C API that is being wrapped)
grep -n "GetOption\|SetOption" sherpa-onnx/c-api/c-api.h 2>/dev/null | head -20

Repository: k2-fsa/sherpa-onnx

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Check the implementation of GetOption and SetOption in cxx-api.cc
grep -n -A 5 "GetOption\|SetOption" sherpa-onnx/c-api/cxx-api.cc | head -60

Repository: k2-fsa/sherpa-onnx

Length of output: 902


🏁 Script executed:

#!/bin/bash
# Check the C API implementation to see what it returns
grep -n -A 5 "SherpaOnnxOnlineStreamGetOption\|SherpaOnnxOfflineStreamGetOption" sherpa-onnx/c-api/c-api.cc | head -40

Repository: k2-fsa/sherpa-onnx

Length of output: 504


🏁 Script executed:

#!/bin/bash
# Check what stream->impl->GetOption returns in online-stream.cc and offline-stream.cc
grep -n "GetOption" sherpa-onnx/csrc/online-stream.cc | head -5
grep -n "GetOption" sherpa-onnx/csrc/offline-stream.cc | head -5

Repository: k2-fsa/sherpa-onnx

Length of output: 750


🏁 Script executed:

#!/bin/bash
# Get the actual implementation
grep -n -A 3 'GetOption.*{' sherpa-onnx/csrc/online-stream.cc

Repository: k2-fsa/sherpa-onnx

Length of output: 756


Return owning std::string from GetOption() — this is a lifetime safety issue.

GetOption() currently returns a borrowed const char * pointing to internal map storage that can be invalidated when SetOption() is called (reallocating the options map), when the stream is destroyed, or under concurrent access. This exposes a use-after-free risk to C++ callers. Return std::string to own the data safely.

Also, SetOption() should take const std::string & parameters for consistency with the rest of the API (e.g., CreateStream(const std::string &hotwords)).

Proposed fix
-  void SetOption(const char *key, const char *value) const;
-  const char *GetOption(const char *key) const;
+  void SetOption(const std::string &key, const std::string &value) const;
+  std::string GetOption(const std::string &key) const;

Also applies to: 382–383 (OfflineStream)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/c-api/cxx-api.h` around lines 182 - 183, Change the API to avoid
returning borrowed C strings and to use std::string parameters: update
SetOption(const char *key, const char *value) to SetOption(const std::string
&key, const std::string &value) and change GetOption(const char *key) const to
return std::string (not const char*) so the caller owns the returned data; apply
the same changes for the OfflineStream variants (the other SetOption/GetOption
declarations) and ensure implementations copy/construct std::string from
internal storage rather than returning pointers into internal containers.

Comment on lines +243 to +257
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}

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 | 🟡 Minor

Unhandled exceptions from std::stoi/std::stof may propagate to C callers.

GetOptionInt and GetOptionFloat use std::stoi and std::stof, which throw std::invalid_argument or std::out_of_range if the stored string is not a valid number. Since these are called through the C API, uncaught exceptions could cause crashes.

Consider catching exceptions and returning the default value:

🛡️ Proposed fix for exception safety
   int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
     auto it = options_.find(key);
     if (it != options_.end()) {
-      return std::stoi(it->second);
+      try {
+        return std::stoi(it->second);
+      } catch (const std::exception &) {
+        return default_value;
+      }
     }
     return default_value;
   }

   float GetOptionFloat(const std::string &key, float default_value) const {
     auto it = options_.find(key);
     if (it != options_.end()) {
-      return std::stof(it->second);
+      try {
+        return std::stof(it->second);
+      } catch (const std::exception &) {
+        return default_value;
+      }
     }
     return default_value;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}
float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
try {
return std::stoi(it->second);
} catch (const std::exception &) {
return default_value;
}
}
return default_value;
}
float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
try {
return std::stof(it->second);
} catch (const std::exception &) {
return default_value;
}
}
return default_value;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/csrc/offline-stream.cc` around lines 243 - 257, GetOptionInt and
GetOptionFloat can throw from std::stoi/std::stof and must not let exceptions
escape into C callers; wrap the calls to std::stoi and std::stof in try/catch
blocks inside GetOptionInt and GetOptionFloat, catch std::invalid_argument and
std::out_of_range (optionally std::exception as a fallback), and return
default_value when parsing fails; keep the same lookup via options_ and only
attempt parsing if the key exists, otherwise return default_value as before.

Comment on lines +240 to +246
// For non-final chunks the original code uses chunk_size_ - 1 to create
// 1-frame overlap. For the final short chunk we consume all frames.
if (s->IsParaformerFinalChunk()) {
s->GetNumProcessedFrames() += actual_chunk_size;
} else {
s->GetNumProcessedFrames() += chunk_size_ - 1;
}

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

Keep the 1-frame overlap until the actual short final chunk.

This branch now advances by chunk_size_ for every final-chunk decode, even when a full chunk is still available. That drops the normal overlap on the remaining full chunks in the drain loop and can change the final recognition result.

💡 Suggested fix
-    if (s->IsParaformerFinalChunk()) {
+    if (s->IsParaformerFinalChunk() && actual_chunk_size < chunk_size_) {
       s->GetNumProcessedFrames() += actual_chunk_size;
     } else {
       s->GetNumProcessedFrames() += chunk_size_ - 1;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/csrc/online-recognizer-paraformer-impl.h` around lines 240 - 246,
The current branch advances GetNumProcessedFrames() by actual_chunk_size
whenever s->IsParaformerFinalChunk(), which erroneously removes the 1-frame
overlap for final-chunk decodes that are actually full-size; change the logic in
online-recognizer-paraformer-impl.h so that GetNumProcessedFrames() is increased
by actual_chunk_size only when the final chunk is truly short (actual_chunk_size
< chunk_size_), otherwise continue to advance by chunk_size_ - 1; update the
branch around s->IsParaformerFinalChunk(), actual_chunk_size, chunk_size_, and
GetNumProcessedFrames() to reflect this conditional behavior.

Comment on lines +159 to +172
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;

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

🧩 Analysis chain

🌐 Web query:

Does C++ std::stoi/std::stof throw exceptions on malformed or out-of-range input, and which exceptions are thrown?

💡 Result:

Yes.

  • Malformed input (i.e., no conversion could be performed, such as the string not starting with a valid number): both std::stoi and std::stof throw std::invalid_argument. [1][2]
  • Out-of-range input (the parsed value can’t be represented in the function’s return type, or the underlying C conversion reports range error by setting errno to ERANGE): both throw std::out_of_range. For stoi this is via strtol/strtoll; for stof via strtof/strtod/strtold. [1][2]

Note: if there is a valid numeric prefix (e.g. "123abc"), conversion succeeds; you can use the optional pos out-parameter to detect trailing junk. [1][2]

Sources: [1] [2]


🏁 Script executed:

head -n 180 sherpa-onnx/csrc/online-stream.cc | tail -n 30

Repository: k2-fsa/sherpa-onnx

Length of output: 872


Wrap type conversions in exception handlers to honor default_value.

SetOption() accepts arbitrary strings, but std::stoi / std::stof throw std::invalid_argument on malformed input and std::out_of_range on values that cannot be represented in the target type. These exceptions propagate uncaught, rendering the default_value parameter useless and causing unexpected crashes.

Suggested fix
  int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
+      try {
         return std::stoi(it->second);
+      } catch (...) {
+        return default_value;
+      }
    }
    return default_value;
  }

  float GetOptionFloat(const std::string &key, float default_value) const {
    auto it = options_.find(key);
    if (it != options_.end()) {
+      try {
         return std::stof(it->second);
+      } catch (...) {
+        return default_value;
+      }
    }
    return default_value;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sherpa-onnx/csrc/online-stream.cc` around lines 159 - 172, GetOptionInt and
GetOptionFloat currently call std::stoi/std::stof directly and will throw
exceptions for malformed or out-of-range strings, so wrap the conversions in
try/catch blocks that catch std::invalid_argument and std::out_of_range (or
std::exception) and return default_value on error; specifically, in
GetOptionInt(const std::string &key, int32_t default_value) and
GetOptionFloat(const std::string &key, float default_value) check
options_.find(key) as before, then attempt the std::stoi/std::stof call inside a
try block and return the parsed value on success, but return default_value in
the catch handler.

Copilot AI 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.

Pull request overview

This PR adds a generic per-stream key-value option mechanism (SetOption/GetOption) to both OnlineStream and OfflineStream, exposed through the C++ core, C API, CXX wrapper, and Python bindings. It also adds streaming Paraformer "final chunk" support, enabling short chunk acceptance and CIF tail token flushing for the last audio segment.

Changes:

  • Added SetOption/GetOption/HasOption/GetOptionInt/GetOptionFloat methods to OnlineStream and OfflineStream at all API layers (core C++, C API, CXX wrapper)
  • Added SetParaformerFinalChunk/IsParaformerFinalChunk for streaming Paraformer final chunk handling, including short chunk padding, CIF tail flush, and SOS/EOS token filtering
  • Exported new C API symbols for macOS

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sherpa-onnx/csrc/online-stream.h Declares new option and paraformer final chunk methods
sherpa-onnx/csrc/online-stream.cc Implements option storage and paraformer final chunk in Impl
sherpa-onnx/csrc/offline-stream.h Declares option methods for OfflineStream
sherpa-onnx/csrc/offline-stream.cc Implements option storage in OfflineStream::Impl
sherpa-onnx/csrc/online-recognizer-paraformer-impl.h Adds final chunk logic: short chunk acceptance, padding, CIF tail flush, SOS/EOS filtering
sherpa-onnx/c-api/c-api.h Declares C API functions for SetFinalChunk, SetOption, GetOption
sherpa-onnx/c-api/c-api.cc Implements C API wrappers
sherpa-onnx/c-api/cxx-api.h Declares SetOption/GetOption on CXX wrapper classes
sherpa-onnx/c-api/cxx-api.cc Implements CXX wrapper forwarding to C API
sherpa-onnx/c-api/sherpa-onnx-symbols-c.exp Exports new symbols for macOS
sherpa-onnx/python/csrc/online-stream.cc Adds Python binding for set_paraformer_final_chunk

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +243 to +257
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}
Comment on lines +182 to +184
void SetOption(const char *key, const char *value) const;
const char *GetOption(const char *key) const;

Comment thread sherpa-onnx/csrc/online-stream.cc Outdated
Comment on lines +134 to +173
void SetParaformerFinalChunk(bool is_final) {
paraformer_is_final_ = is_final;
}

bool IsParaformerFinalChunk() const {
return paraformer_is_final_;
}

void SetOption(const std::string &key, const std::string &value) {
options_[key] = value;
}

bool HasOption(const std::string &key) const {
return options_.count(key) != 0;
}

const std::string &GetOption(const std::string &key) const {
auto it = options_.find(key);
if (it != options_.end()) {
return it->second;
}
static const std::string kEmpty;
return kEmpty;
}

int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}
Comment on lines +159 to +173
int32_t GetOptionInt(const std::string &key, int32_t default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stoi(it->second);
}
return default_value;
}

float GetOptionFloat(const std::string &key, float default_value) const {
auto it = options_.find(key);
if (it != options_.end()) {
return std::stof(it->second);
}
return default_value;
}
@ZhaoChaoqun
ZhaoChaoqun force-pushed the feat/setoption-cxx-wrapper branch from fc0be49 to 753da15 Compare March 18, 2026 08:17
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Mar 18, 2026
@csukuangfj

Copy link
Copy Markdown
Collaborator

Please first fix the comments in the PR for C API. After the PR for C API is merged, please update this PR to include only code for C++ API.

Add SetOption/GetOption methods to the C++ wrapper (cxx-api) for both
OnlineStream and OfflineStream, calling the C API functions.

Depends on: feat/setoption-c-api
Ref: k2-fsa#3101

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ZhaoChaoqun
ZhaoChaoqun force-pushed the feat/setoption-cxx-wrapper branch from 753da15 to e270553 Compare March 18, 2026 08:59
@ZhaoChaoqun

Copy link
Copy Markdown
Contributor Author

@csukuangfj Rebased onto latest master (with #3308 merged) and added HasOption CXX wrapper for both OnlineStream and OfflineStream. Could you please review when you have a chance? Thanks!

@csukuangfj csukuangfj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for your contribution!

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

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants