Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ModelLoadError,
NotImplementedByModel,
OutOfMemory,
OutputRepetition,
OutputTruncated,
TranscribeError,
UnsupportedRequest,
Expand Down Expand Up @@ -108,6 +109,7 @@
"Aborted",
"InputTooLong",
"OutputTruncated",
"OutputRepetition",
"native_version",
"native_commit",
"library_path",
Expand Down Expand Up @@ -1112,9 +1114,9 @@ def run(self, pcm: PCMLike, *, task: Task = "transcribe",
capabilities advertise ``supports_spec_decode`` (-1 = family default,
0 = disabled, >0 = draft length; silently ignored elsewhere).

On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` the
partial transcript is preserved and attached to the exception as
``partial_result``."""
On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` (including
its ``OutputRepetition`` subclass) the partial transcript is preserved
and attached to the exception as ``partial_result``."""
self._cancel.clear()
array, n_samples = _pcm_to_carray(pcm)
params = _build_run_params(task, language, target_language, timestamps,
Expand All @@ -1128,7 +1130,8 @@ def run(self, pcm: PCMLike, *, task: Task = "transcribe",
"transcribe_run")
except (Aborted, OutputTruncated) as exc:
# The C API preserves the partial transcript on the session for
# exactly these two statuses; surface it rather than discard it.
# these statuses (OutputRepetition included, as an OutputTruncated
# subclass); surface it rather than discard it.
exc.partial_result = self._materialize()
raise
return self._materialize()
Expand Down
3 changes: 2 additions & 1 deletion bindings/python/src/transcribe_cpp/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Stable digest of the ABI surface below (structs, enums, macros, layout,
# prototypes). A native provider package echoes this back so the API
# package can reject an ABI-mismatched provider before dlopen.
PUBLIC_HEADER_HASH = "7df72bf9e667b8c2"
PUBLIC_HEADER_HASH = "9866413f80138057"

# === enum constants ===
TRANSCRIBE_OK = 0
Expand All @@ -35,6 +35,7 @@
TRANSCRIBE_ERR_UNSUPPORTED_ITN = 16
TRANSCRIBE_ERR_INPUT_TOO_LONG = 17
TRANSCRIBE_ERR_OUTPUT_TRUNCATED = 18
TRANSCRIBE_ERR_OUTPUT_REPETITION = 19
TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0
TRANSCRIBE_ABI_SESSION_PARAMS = 1
TRANSCRIBE_ABI_RUN_PARAMS = 2
Expand Down
13 changes: 13 additions & 0 deletions bindings/python/src/transcribe_cpp/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TRANSCRIBE_ERR_INVALID_ARG as ERR_INVALID_ARG,
TRANSCRIBE_ERR_NOT_IMPLEMENTED as ERR_NOT_IMPLEMENTED,
TRANSCRIBE_ERR_OOM as ERR_OOM,
TRANSCRIBE_ERR_OUTPUT_REPETITION as ERR_OUTPUT_REPETITION,
TRANSCRIBE_ERR_OUTPUT_TRUNCATED as ERR_OUTPUT_TRUNCATED,
TRANSCRIBE_ERR_SAMPLE_RATE as ERR_SAMPLE_RATE,
TRANSCRIBE_ERR_UNSUPPORTED_ARCH as ERR_UNSUPPORTED_ARCH,
Expand Down Expand Up @@ -113,6 +114,17 @@ class OutputTruncated(TranscribeError):
partial_result: "Optional[Result]" = None


class OutputRepetition(OutputTruncated):
"""The decode was stopped because the output began repeating itself —
the transcript is incomplete by contract.

A subclass of :class:`OutputTruncated`, so a handler for incomplete
transcripts catches both. ``partial_result`` holds the partial transcript
with the repeats dropped (one copy kept), or None when the status surfaced
outside a result-bearing call.
"""


_STATUS_TO_EXC = {
ERR_INVALID_ARG: InvalidArgument,
ERR_NOT_IMPLEMENTED: NotImplementedByModel,
Expand All @@ -132,6 +144,7 @@ class OutputTruncated(TranscribeError):
ERR_UNSUPPORTED_ITN: UnsupportedRequest,
ERR_INPUT_TOO_LONG: InputTooLong,
ERR_OUTPUT_TRUNCATED: OutputTruncated,
ERR_OUTPUT_REPETITION: OutputRepetition,
}


Expand Down
9 changes: 9 additions & 0 deletions bindings/python/tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_every_status_maps_to_documented_subclass():
errors.ERR_UNSUPPORTED_ITN: t.UnsupportedRequest,
errors.ERR_INPUT_TOO_LONG: t.InputTooLong,
errors.ERR_OUTPUT_TRUNCATED: t.OutputTruncated,
errors.ERR_OUTPUT_REPETITION: t.OutputRepetition,
}
# The mapping table covers every non-OK status the header defines, and
# nothing else (a new C status must be mapped deliberately, not by
Expand All @@ -64,6 +65,14 @@ def test_unknown_status_degrades_to_base_class():
assert exc.status == 999


def test_output_repetition_is_an_output_truncated():
# A handler that keeps the partial of an incomplete transcript catches both.
exc = errors.exception_for_status(errors.ERR_OUTPUT_REPETITION, "looped", "run")
assert isinstance(exc, t.OutputRepetition)
assert isinstance(exc, t.OutputTruncated)
assert exc.partial_result is None


def test_exception_for_status_builds_without_raising():
exc = errors.exception_for_status(errors.ERR_ABORTED, "aborted", "run")
assert isinstance(exc, t.Aborted)
Expand Down
5 changes: 3 additions & 2 deletions bindings/rust/sys/src/transcribe_sys.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @generated by `cargo xtask bindgen` from include/transcribe/extensions.h
// DO NOT EDIT BY HAND. Regenerate: `cargo xtask bindgen`.
// Pinned to include/transcribe.abihash = 7df72bf9e667b8c2
// Pinned to include/transcribe.abihash = 9866413f80138057

/// The public-ABI digest these bindings were generated against
/// (sha256/16 over the normalized FFI surface). The load-time version
/// gate and the CI drift check both anchor on this value.
pub const PUBLIC_HEADER_HASH: &str = "7df72bf9e667b8c2";
pub const PUBLIC_HEADER_HASH: &str = "9866413f80138057";

/* automatically generated by rust-bindgen 0.72.1 */

Expand Down Expand Up @@ -35,6 +35,7 @@ impl transcribe_status {
pub const TRANSCRIBE_ERR_UNSUPPORTED_ITN: transcribe_status = transcribe_status(16);
pub const TRANSCRIBE_ERR_INPUT_TOO_LONG: transcribe_status = transcribe_status(17);
pub const TRANSCRIBE_ERR_OUTPUT_TRUNCATED: transcribe_status = transcribe_status(18);
pub const TRANSCRIBE_ERR_OUTPUT_REPETITION: transcribe_status = transcribe_status(19);
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
Expand Down
23 changes: 19 additions & 4 deletions bindings/rust/transcribe-cpp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ pub enum Error {
/// The (incomplete) transcript produced before truncation.
partial: Option<Box<Transcript>>,
},
/// `TRANSCRIBE_ERR_OUTPUT_REPETITION` — the decode was stopped because the
/// output began repeating itself; the transcript is incomplete by contract.
/// The partial transcript, with the repeats dropped, is always preserved.
#[error("output began repeating before end-of-stream: {message}")]
OutputRepetition {
message: String,
/// The (incomplete) transcript produced before the loop, one copy kept.
partial: Option<Box<Transcript>>,
},
/// The loaded library's base version disagrees with the headers this crate
/// was generated against (the pre-1.0 version lock). Raised on first use.
#[error("native library version mismatch: {0}")]
Expand Down Expand Up @@ -103,18 +112,20 @@ impl Error {
Error::InputTooLong(_) => S::TRANSCRIBE_ERR_INPUT_TOO_LONG,
Error::Aborted { .. } => S::TRANSCRIBE_ERR_ABORTED,
Error::OutputTruncated { .. } => S::TRANSCRIBE_ERR_OUTPUT_TRUNCATED,
Error::OutputRepetition { .. } => S::TRANSCRIBE_ERR_OUTPUT_REPETITION,
_ => S::TRANSCRIBE_OK,
};
s.0 as i32
}

/// The partial transcript carried by [`Error::Aborted`] /
/// [`Error::OutputTruncated`], if any. `None` for every other variant.
/// [`Error::OutputTruncated`] / [`Error::OutputRepetition`], if any. `None`
/// for every other variant.
pub fn partial(&self) -> Option<&Transcript> {
match self {
Error::Aborted { partial, .. } | Error::OutputTruncated { partial, .. } => {
partial.as_deref()
}
Error::Aborted { partial, .. }
| Error::OutputTruncated { partial, .. }
| Error::OutputRepetition { partial, .. } => partial.as_deref(),
_ => None,
}
}
Expand Down Expand Up @@ -171,6 +182,10 @@ pub(crate) fn error_for_status(status: sys::transcribe_status, context: &str) ->
message: msg,
partial: None,
},
S::TRANSCRIBE_ERR_OUTPUT_REPETITION => Error::OutputRepetition {
message: msg,
partial: None,
},
_ => Error::Other(msg),
}
}
Expand Down
24 changes: 16 additions & 8 deletions bindings/rust/transcribe-cpp/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,18 @@ impl Session {
unsafe { sys::transcribe_was_aborted(self.ptr) }
}

/// Whether the most recent decode stopped at the generation budget before
/// end-of-stream (the transcript is incomplete).
/// Whether the most recent decode stopped before end-of-stream, at the
/// generation budget or because the output began repeating (the transcript
/// is incomplete).
pub fn was_truncated(&self) -> bool {
unsafe { sys::transcribe_was_truncated(self.ptr) }
}

/// Transcribe one buffer of 16 kHz mono float32 PCM in `[-1, 1]`.
///
/// On an aborted or truncated decode the partial transcript is preserved
/// on the returned [`Error::Aborted`] / [`Error::OutputTruncated`].
/// On an aborted, truncated, or repetition-stopped decode the partial
/// transcript is preserved on the returned [`Error::Aborted`] /
/// [`Error::OutputTruncated`] / [`Error::OutputRepetition`].
pub fn run(&mut self, pcm: &[f32], options: &RunOptions) -> Result<Transcript> {
let (params, _lang, _target, _family) = build_run_params(options)?;
let n = clamp_len(pcm.len())?;
Expand All @@ -171,9 +173,10 @@ impl Session {
match status {
s if s == sys::transcribe_status::TRANSCRIBE_OK => Ok(self.materialize_run()),
s if s == sys::transcribe_status::TRANSCRIBE_ERR_ABORTED
|| s == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_TRUNCATED =>
|| s == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_TRUNCATED
|| s == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_REPETITION =>
{
// Partial transcript is preserved by the C API for both.
// Partial transcript is preserved by the C API for all three.
let partial = Box::new(self.materialize_run());
Err(attach_partial(error_for_status(s, "run"), partial))
}
Expand Down Expand Up @@ -229,6 +232,7 @@ impl Session {
results.push(Ok(self.materialize_batch(i)));
} else if st == sys::transcribe_status::TRANSCRIBE_ERR_ABORTED
|| st == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_TRUNCATED
|| st == sys::transcribe_status::TRANSCRIBE_ERR_OUTPUT_REPETITION
{
let partial = Box::new(self.materialize_batch(i));
let err = attach_partial(error_for_status(st, "run_batch utterance"), partial);
Expand Down Expand Up @@ -460,8 +464,8 @@ fn clamp_len(len: usize) -> Result<i32> {
i32::try_from(len).map_err(|_| Error::InvalidArgument(format!("length {len} exceeds i32::MAX")))
}

/// Replace the (partial-less) Aborted/OutputTruncated error with one carrying
/// the materialized partial transcript.
/// Replace the (partial-less) Aborted/OutputTruncated/OutputRepetition error
/// with one carrying the materialized partial transcript.
fn attach_partial(err: Error, partial: Box<Transcript>) -> Error {
match err {
Error::Aborted { message, .. } => Error::Aborted {
Expand All @@ -472,6 +476,10 @@ fn attach_partial(err: Error, partial: Box<Transcript>) -> Error {
message,
partial: Some(partial),
},
Error::OutputRepetition { message, .. } => Error::OutputRepetition {
message,
partial: Some(partial),
},
other => other,
}
}
Expand Down
2 changes: 1 addition & 1 deletion bindings/swift/Sources/TranscribeCpp/ABIHash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import CTranscribe
extension Transcribe {
/// sha256/16 of the normalized public FFI surface, pinned to the value in
/// include/transcribe.abihash at the time this binding was last reviewed.
public static let pinnedHeaderHash = "7df72bf9e667b8c2"
public static let pinnedHeaderHash = "9866413f80138057"

/// The public-ABI digest this binding was reviewed against (16 hex chars).
public static func headerHash() -> String { pinnedHeaderHash }
Expand Down
6 changes: 6 additions & 0 deletions bindings/swift/Sources/TranscribeCpp/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ public final class Session {
case TRANSCRIBE_ERR_OUTPUT_TRUNCATED:
return .failure(TranscribeError.outputTruncated(
message: TranscribeError.message(s, context), partial: batchTranscript(i)))
case TRANSCRIBE_ERR_OUTPUT_REPETITION:
return .failure(TranscribeError.outputRepetition(
message: TranscribeError.message(s, context), partial: batchTranscript(i)))
default:
return .failure(TranscribeError.make(s, context: context))
}
Expand Down Expand Up @@ -176,6 +179,9 @@ public final class Session {
case TRANSCRIBE_ERR_OUTPUT_TRUNCATED:
throw TranscribeError.outputTruncated(
message: TranscribeError.message(status, context), partial: readTranscript())
case TRANSCRIBE_ERR_OUTPUT_REPETITION:
throw TranscribeError.outputRepetition(
message: TranscribeError.message(status, context), partial: readTranscript())
default:
throw TranscribeError.make(status, context: context)
}
Expand Down
31 changes: 30 additions & 1 deletion bindings/swift/Sources/TranscribeCpp/TranscribeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import CTranscribe
/// failures stay distinct (requirements §3): "no such provider" is not
/// "provider can't satisfy this request".
///
/// `.aborted` / `.outputTruncated` carry the preserved partial `Transcript`
/// `.aborted` / `.outputTruncated` / `.outputRepetition` carry the preserved partial `Transcript`
/// (the C side keeps partial output readable after those statuses); it is `nil`
/// when the error is built outside a run (e.g. by `check`).
///
Expand All @@ -25,6 +25,9 @@ public enum TranscribeError: Error {
case inputTooLong(String)
case aborted(message: String, partial: Transcript?)
case outputTruncated(message: String, partial: Transcript?)
/// The decode was stopped because the output began repeating itself; the
/// repeats are dropped from `partial`, which is incomplete.
case outputRepetition(message: String, partial: Transcript?)
case versionMismatch(String)
case busy(String)
case other(status: Int32, message: String)
Expand Down Expand Up @@ -64,11 +67,37 @@ public enum TranscribeError: Error {
return .aborted(message: message, partial: nil)
case TRANSCRIBE_ERR_OUTPUT_TRUNCATED:
return .outputTruncated(message: message, partial: nil)
case TRANSCRIBE_ERR_OUTPUT_REPETITION:
return .outputRepetition(message: message, partial: nil)
default:
return .other(status: raw, message: message)
}
}

/// The partial transcript carried by `.aborted` / `.outputTruncated` /
/// `.outputRepetition`, if any; `nil` for every other case.
public var partial: Transcript? {
switch self {
case .aborted(_, let partial), .outputTruncated(_, let partial), .outputRepetition(_, let partial):
return partial
default:
return nil
}
}

/// True when the decode stopped before end-of-stream, at the generation
/// budget (`.outputTruncated`) or because the output began repeating
/// (`.outputRepetition`), as `transcribe_was_truncated` reports it. The
/// transcript in `partial` is incomplete.
public var isTruncated: Bool {
switch self {
case .outputTruncated, .outputRepetition:
return true
default:
return false
}
}

/// Throw the mapped error unless `status` is `TRANSCRIBE_OK`.
static func check(_ status: transcribe_status, context: String = "") throws {
guard status != TRANSCRIBE_OK else { return }
Expand Down
10 changes: 10 additions & 0 deletions bindings/swift/Tests/TranscribeCppTests/NoModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ final class NoModelTests: XCTestCase {
XCTAssertFalse(Transcribe.statusString(3).isEmpty) // ERR_FILE_NOT_FOUND
}

func testTruncationStatusesShareOneCheck() {
// A handler for incomplete transcripts covers both cut-short statuses.
XCTAssertTrue(TranscribeError.outputTruncated(message: "", partial: nil).isTruncated)
XCTAssertTrue(TranscribeError.outputRepetition(message: "", partial: nil).isTruncated)
XCTAssertFalse(TranscribeError.aborted(message: "", partial: nil).isTruncated)
XCTAssertFalse(TranscribeError.inputTooLong("").isTruncated)
XCTAssertNil(TranscribeError.outputRepetition(message: "", partial: nil).partial)
XCTAssertNil(TranscribeError.inputTooLong("").partial)
}

func testAtLeastOneDevice() {
XCTAssertGreaterThanOrEqual(Transcribe.devices().count, 1)
}
Expand Down
3 changes: 2 additions & 1 deletion bindings/typescript/src/_generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// Stable digest of the ABI surface (structs, enums, macros, layout,
// prototypes), computed by the Python oracle and pinned here so a header
// ABI change turns this binding's drift check red for conscious review.
export const PUBLIC_HEADER_HASH = "7df72bf9e667b8c2";
export const PUBLIC_HEADER_HASH = "9866413f80138057";

// === enum constants ===
export const TRANSCRIBE_OK = 0;
Expand All @@ -33,6 +33,7 @@ export const TRANSCRIBE_ERR_UNSUPPORTED_PNC = 15;
export const TRANSCRIBE_ERR_UNSUPPORTED_ITN = 16;
export const TRANSCRIBE_ERR_INPUT_TOO_LONG = 17;
export const TRANSCRIBE_ERR_OUTPUT_TRUNCATED = 18;
export const TRANSCRIBE_ERR_OUTPUT_REPETITION = 19;
export const TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0;
export const TRANSCRIBE_ABI_SESSION_PARAMS = 1;
export const TRANSCRIBE_ABI_RUN_PARAMS = 2;
Expand Down
Loading
Loading