From 6e466092cb057b2654fe310cb367a848be93d3be Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 9 Jun 2026 15:29:10 +0800 Subject: [PATCH 01/34] placeholder bindings --- bindings/python/LICENSE | 21 +++++++++++ bindings/python/README.md | 19 ++++++++++ bindings/python/pyproject.toml | 30 +++++++++++++++ .../python/src/transcribe_cpp/__init__.py | 12 ++++++ bindings/rust/.gitignore | 2 + bindings/rust/Cargo.toml | 15 ++++++++ bindings/rust/LICENSE | 21 +++++++++++ bindings/rust/README.md | 11 ++++++ bindings/rust/src/lib.rs | 8 ++++ bindings/swift/.gitignore | 3 ++ bindings/swift/LICENSE | 21 +++++++++++ bindings/swift/Package.swift | 20 ++++++++++ .../Sources/TranscribeCpp/TranscribeCpp.swift | 11 ++++++ bindings/typescript/.gitignore | 3 ++ bindings/typescript/LICENSE | 21 +++++++++++ bindings/typescript/README.md | 17 +++++++++ bindings/typescript/bun.lock | 15 ++++++++ bindings/typescript/package.json | 37 +++++++++++++++++++ bindings/typescript/src/index.ts | 11 ++++++ bindings/typescript/tsconfig.json | 14 +++++++ 20 files changed, 312 insertions(+) create mode 100644 bindings/python/LICENSE create mode 100644 bindings/python/README.md create mode 100644 bindings/python/pyproject.toml create mode 100644 bindings/python/src/transcribe_cpp/__init__.py create mode 100644 bindings/rust/.gitignore create mode 100644 bindings/rust/Cargo.toml create mode 100644 bindings/rust/LICENSE create mode 100644 bindings/rust/README.md create mode 100644 bindings/rust/src/lib.rs create mode 100644 bindings/swift/.gitignore create mode 100644 bindings/swift/LICENSE create mode 100644 bindings/swift/Package.swift create mode 100644 bindings/swift/Sources/TranscribeCpp/TranscribeCpp.swift create mode 100644 bindings/typescript/.gitignore create mode 100644 bindings/typescript/LICENSE create mode 100644 bindings/typescript/README.md create mode 100644 bindings/typescript/bun.lock create mode 100644 bindings/typescript/package.json create mode 100644 bindings/typescript/src/index.ts create mode 100644 bindings/typescript/tsconfig.json diff --git a/bindings/python/LICENSE b/bindings/python/LICENSE new file mode 100644 index 00000000..38c7ccc7 --- /dev/null +++ b/bindings/python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The transcribe.cpp authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/python/README.md b/bindings/python/README.md new file mode 100644 index 00000000..a25c9f5c --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,19 @@ +# transcribe-cpp + +Python bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp), +a C/C++ speech-to-text library built on ggml. + +> **Status: placeholder.** This release reserves the `transcribe-cpp` name on +> PyPI while the first-party bindings are developed. It ships no native code and +> exposes no transcription API yet. Watch the repository for the first +> functional release. + +```python +import transcribe_cpp + +print(transcribe_cpp.__version__) +``` + +- Import package: `transcribe_cpp` +- Distribution: `transcribe-cpp` +- License: MIT diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 00000000..a2c73c37 --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "transcribe-cpp" +version = "0.0.0" +description = "Python bindings for transcribe.cpp (pre-release name reservation placeholder)" +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "The transcribe.cpp authors" }] +keywords = ["transcription", "speech-to-text", "asr", "ggml", "whisper", "parakeet"] +classifiers = [ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] + +[project.urls] +Homepage = "https://github.com/handy-computer/transcribe.cpp" +Repository = "https://github.com/handy-computer/transcribe.cpp" +Issues = "https://github.com/handy-computer/transcribe.cpp/issues" + +[tool.hatch.build.targets.wheel] +packages = ["src/transcribe_cpp"] diff --git a/bindings/python/src/transcribe_cpp/__init__.py b/bindings/python/src/transcribe_cpp/__init__.py new file mode 100644 index 00000000..f60d7554 --- /dev/null +++ b/bindings/python/src/transcribe_cpp/__init__.py @@ -0,0 +1,12 @@ +"""Python bindings for transcribe.cpp. + +This is a placeholder distribution that reserves the ``transcribe-cpp`` name on +PyPI while first-party bindings are under development. It intentionally ships no +native code yet, so importing it succeeds but no transcription API is available. + +See https://github.com/handy-computer/transcribe.cpp for status. +""" + +__version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/bindings/rust/.gitignore b/bindings/rust/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/bindings/rust/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 00000000..dc002af8 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "transcribe-cpp" +version = "0.0.0" +edition = "2021" +description = "Rust bindings for transcribe.cpp (pre-release name reservation placeholder)" +license = "MIT" +readme = "README.md" +repository = "https://github.com/handy-computer/transcribe.cpp" +homepage = "https://github.com/handy-computer/transcribe.cpp" +keywords = ["transcription", "speech", "asr", "stt", "ggml"] +categories = ["multimedia::audio", "api-bindings"] + +[lib] +name = "transcribe_cpp" +path = "src/lib.rs" diff --git a/bindings/rust/LICENSE b/bindings/rust/LICENSE new file mode 100644 index 00000000..38c7ccc7 --- /dev/null +++ b/bindings/rust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The transcribe.cpp authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 00000000..b33088e0 --- /dev/null +++ b/bindings/rust/README.md @@ -0,0 +1,11 @@ +# transcribe-cpp + +Rust bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp), +a C/C++ speech-to-text library built on ggml. + +> **Status: placeholder.** This release reserves the `transcribe-cpp` name on +> crates.io while the first-party bindings are developed. It ships no +> functionality yet. Watch the repository for the first functional release. + +- Crate: `transcribe-cpp` +- License: MIT diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 00000000..09488f2c --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -0,0 +1,8 @@ +//! Rust bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp). +//! +//! This is a placeholder crate that reserves the `transcribe-cpp` name on +//! crates.io while first-party bindings are developed. It ships no +//! functionality yet. + +/// Version of this placeholder crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/bindings/swift/.gitignore b/bindings/swift/.gitignore new file mode 100644 index 00000000..06b99482 --- /dev/null +++ b/bindings/swift/.gitignore @@ -0,0 +1,3 @@ +.build +.swiftpm +Package.resolved diff --git a/bindings/swift/LICENSE b/bindings/swift/LICENSE new file mode 100644 index 00000000..38c7ccc7 --- /dev/null +++ b/bindings/swift/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The transcribe.cpp authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/swift/Package.swift b/bindings/swift/Package.swift new file mode 100644 index 00000000..023aa67e --- /dev/null +++ b/bindings/swift/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 +import PackageDescription + +// Placeholder Swift package for transcribe.cpp bindings. +// +// SwiftPM has no central name registry: packages are identified by their Git +// URL, so the "name" you reserve is really the repository URL plus the product +// name below. To be consumable as a remote dependency, a Package.swift must +// live at the ROOT of a git repository (a subdirectory package can only be used +// as a local `path:` dependency), so this skeleton is expected to move to the +// root of a dedicated bindings repo before publishing. +let package = Package( + name: "transcribe-cpp", + products: [ + .library(name: "TranscribeCpp", targets: ["TranscribeCpp"]), + ], + targets: [ + .target(name: "TranscribeCpp"), + ] +) diff --git a/bindings/swift/Sources/TranscribeCpp/TranscribeCpp.swift b/bindings/swift/Sources/TranscribeCpp/TranscribeCpp.swift new file mode 100644 index 00000000..b23bc51b --- /dev/null +++ b/bindings/swift/Sources/TranscribeCpp/TranscribeCpp.swift @@ -0,0 +1,11 @@ +/// Swift bindings for transcribe.cpp. +/// +/// This is a placeholder package skeleton for the `transcribe-cpp` Swift +/// bindings while the first-party implementation is developed. It ships no +/// functionality yet. +/// +/// See https://github.com/handy-computer/transcribe.cpp for status. +public enum TranscribeCpp { + /// Version of this placeholder package. + public static let version = "0.0.0" +} diff --git a/bindings/typescript/.gitignore b/bindings/typescript/.gitignore new file mode 100644 index 00000000..69694e4b --- /dev/null +++ b/bindings/typescript/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +*.tgz diff --git a/bindings/typescript/LICENSE b/bindings/typescript/LICENSE new file mode 100644 index 00000000..38c7ccc7 --- /dev/null +++ b/bindings/typescript/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The transcribe.cpp authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/typescript/README.md b/bindings/typescript/README.md new file mode 100644 index 00000000..4252e8a0 --- /dev/null +++ b/bindings/typescript/README.md @@ -0,0 +1,17 @@ +# transcribe-cpp + +TypeScript/Node.js bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp), +a C/C++ speech-to-text library built on ggml. + +> **Status: placeholder.** This release reserves the `transcribe-cpp` name on +> npm while the first-party bindings are developed. It ships no functionality +> yet. Watch the repository for the first functional release. + +```ts +import { version } from "transcribe-cpp"; + +console.log(version); +``` + +- Package: `transcribe-cpp` +- License: MIT diff --git a/bindings/typescript/bun.lock b/bindings/typescript/bun.lock new file mode 100644 index 00000000..4ccd0fe8 --- /dev/null +++ b/bindings/typescript/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "transcribe-cpp", + "devDependencies": { + "typescript": "^5.6.0", + }, + }, + }, + "packages": { + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + } +} diff --git a/bindings/typescript/package.json b/bindings/typescript/package.json new file mode 100644 index 00000000..18ed88bb --- /dev/null +++ b/bindings/typescript/package.json @@ -0,0 +1,37 @@ +{ + "name": "transcribe-cpp", + "version": "0.0.0", + "description": "TypeScript/Node.js bindings for transcribe.cpp (pre-release name reservation placeholder)", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist", "README.md", "LICENSE"], + "license": "MIT", + "author": "The transcribe.cpp authors", + "homepage": "https://github.com/handy-computer/transcribe.cpp#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/handy-computer/transcribe.cpp.git" + }, + "bugs": { + "url": "https://github.com/handy-computer/transcribe.cpp/issues" + }, + "keywords": ["transcription", "speech-to-text", "asr", "stt", "ggml", "whisper", "parakeet"], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsc", + "prepublishOnly": "bun run build" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/bindings/typescript/src/index.ts b/bindings/typescript/src/index.ts new file mode 100644 index 00000000..52db35ec --- /dev/null +++ b/bindings/typescript/src/index.ts @@ -0,0 +1,11 @@ +/** + * TypeScript/Node.js bindings for transcribe.cpp. + * + * This is a placeholder package that reserves the `transcribe-cpp` name on npm + * while first-party bindings are developed. It ships no functionality yet. + * + * @see https://github.com/handy-computer/transcribe.cpp + */ + +/** Version of this placeholder package. */ +export const version = "0.0.0"; diff --git a/bindings/typescript/tsconfig.json b/bindings/typescript/tsconfig.json new file mode 100644 index 00000000..34837c59 --- /dev/null +++ b/bindings/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "types": [] + }, + "include": ["src"] +} From 7daf815cf6490cc006dea41d58de65e3398603ec Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 9 Jun 2026 21:47:34 +0800 Subject: [PATCH 02/34] use proper logger --- src/arch/canary/decoder.cpp | 25 +-- src/arch/canary/encoder.cpp | 11 +- src/arch/canary/model.cpp | 60 +++---- src/arch/canary/weights.cpp | 55 +++---- src/arch/canary_qwen/decoder.cpp | 37 ++--- src/arch/canary_qwen/encoder.cpp | 11 +- src/arch/canary_qwen/model.cpp | 98 ++++++------ src/arch/canary_qwen/weights.cpp | 19 +-- src/arch/cohere/decoder.cpp | 37 ++--- src/arch/cohere/encoder.cpp | 13 +- src/arch/cohere/model.cpp | 82 +++++----- src/arch/cohere/weights.cpp | 53 +++---- src/arch/funasr_nano/adaptor.cpp | 9 +- src/arch/funasr_nano/decoder.cpp | 37 ++--- src/arch/funasr_nano/encoder.cpp | 7 +- src/arch/funasr_nano/model.cpp | 38 ++--- src/arch/funasr_nano/weights.cpp | 75 ++++----- src/arch/gigaam/decoder.cpp | 9 +- src/arch/gigaam/encoder.cpp | 9 +- src/arch/gigaam/model.cpp | 20 +-- src/arch/gigaam/weights.cpp | 25 +-- src/arch/granite/decoder.cpp | 41 ++--- src/arch/granite/encoder.cpp | 7 +- src/arch/granite/model.cpp | 56 +++---- src/arch/granite/projector.cpp | 7 +- src/arch/granite/weights.cpp | 69 +++++---- src/arch/granite_nar/decoder.cpp | 7 +- src/arch/granite_nar/encoder.cpp | 15 +- src/arch/granite_nar/model.cpp | 28 ++-- src/arch/granite_nar/projector.cpp | 9 +- src/arch/granite_nar/weights.cpp | 11 +- src/arch/medasr/encoder.cpp | 9 +- src/arch/medasr/model.cpp | 26 ++-- src/arch/medasr/weights.cpp | 21 +-- src/arch/moonshine/decoder.cpp | 33 ++-- src/arch/moonshine/encoder.cpp | 13 +- src/arch/moonshine/model.cpp | 40 ++--- src/arch/moonshine/weights.cpp | 67 ++++---- src/arch/moonshine_streaming/decoder.cpp | 51 +++--- src/arch/moonshine_streaming/encoder.cpp | 17 +- src/arch/moonshine_streaming/model.cpp | 66 ++++---- src/arch/moonshine_streaming/weights.cpp | 67 ++++---- src/arch/parakeet/decoder.cpp | 97 ++++++------ src/arch/parakeet/encoder.cpp | 49 +++--- src/arch/parakeet/model.cpp | 145 ++++++++--------- src/arch/parakeet/weights.cpp | 147 +++++++++--------- src/arch/qwen3_asr/decoder.cpp | 85 +++++----- src/arch/qwen3_asr/encoder.cpp | 25 +-- src/arch/qwen3_asr/model.cpp | 68 ++++---- src/arch/qwen3_asr/weights.cpp | 59 +++---- src/arch/sensevoice/encoder.cpp | 9 +- src/arch/sensevoice/model.cpp | 18 +-- src/arch/sensevoice/weights.cpp | 41 ++--- src/arch/voxtral/decoder.cpp | 35 +++-- src/arch/voxtral/encoder.cpp | 21 +-- src/arch/voxtral/model.cpp | 34 ++-- src/arch/voxtral/weights.cpp | 47 +++--- src/arch/voxtral_realtime/decoder.cpp | 21 +-- src/arch/voxtral_realtime/encoder.cpp | 29 ++-- src/arch/voxtral_realtime/model.cpp | 44 +++--- src/arch/voxtral_realtime/weights.cpp | 29 ++-- src/arch/whisper/bin_load.cpp | 29 ++-- src/arch/whisper/decoder.cpp | 45 +++--- src/arch/whisper/encoder.cpp | 17 +- src/arch/whisper/model.cpp | 189 ++++++++++++----------- src/arch/whisper/weights.cpp | 81 +++++----- src/causal_lm/causal_lm.cpp | 55 +++---- src/conformer/conformer.cpp | 5 +- src/granite_conformer/shaw_attn.cpp | 5 +- src/transcribe-bin-loader.cpp | 117 +++++++------- src/transcribe-load-common.cpp | 71 ++++----- src/transcribe-meta.cpp | 25 +-- src/transcribe-tokenizer.cpp | 25 +-- src/transcribe-weights-util.cpp | 19 +-- src/transcribe.cpp | 75 ++++++++- 75 files changed, 1639 insertions(+), 1512 deletions(-) diff --git a/src/arch/canary/decoder.cpp b/src/arch/canary/decoder.cpp index 949cfa99..dc1907ee 100644 --- a/src/arch/canary/decoder.cpp +++ b/src/arch/canary/decoder.cpp @@ -14,6 +14,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -306,8 +307,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || T_enc <= 0) { - std::fprintf(stderr, - "canary cross_kv: invalid arg (ctx=%p, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary cross_kv: invalid arg (ctx=%p, T_enc=%d)", static_cast(ctx), T_enc); return db; } @@ -320,7 +321,7 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 4096, false); if (db.graph == nullptr) { - std::fprintf(stderr, "canary cross_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary cross_kv: ggml_new_graph_custom failed"); return db; } @@ -363,9 +364,9 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || n_tokens <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary decoder_kv: invalid arg " - "(ctx=%p, n_tokens=%d, T_enc=%d)\n", + "(ctx=%p, n_tokens=%d, T_enc=%d)", static_cast(ctx), n_tokens, T_enc); return db; } @@ -410,7 +411,7 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, "canary decoder_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary decoder_kv: ggml_new_graph_custom failed"); return db; } @@ -548,14 +549,14 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "canary step: invalid arg (max_n_kv=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary step: invalid arg (max_n_kv=%d, T_enc=%d)", max_n_kv, T_enc); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "canary step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -581,7 +582,7 @@ StepBuild build_step_graph(ggml_context * ctx, sb.graph = ggml_new_graph_custom(ctx, 8192, false); if (sb.graph == nullptr) { - std::fprintf(stderr, "canary step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary step: ggml_new_graph_custom failed"); return sb; } @@ -810,7 +811,7 @@ StepBuildBatched build_step_graph_batched(ggml_context * ctx, sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || T_enc_max <= 0 || n_batch <= 0) return sb; if (!use_flash) { - std::fprintf(stderr, "canary step(batched): requires flash path\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary step(batched): requires flash path"); return sb; } diff --git a/src/arch/canary/encoder.cpp b/src/arch/canary/encoder.cpp index d5af3d33..4de4719f 100644 --- a/src/arch/canary/encoder.cpp +++ b/src/arch/canary/encoder.cpp @@ -14,6 +14,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -108,16 +109,16 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, - "canary encoder: invalid arg (ctx=%p, n_mel_frames=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary encoder: invalid arg (ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } if (hp.enc_subsampling_factor != 8 || hp.fe_num_mels != 128) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary encoder: unsupported geometry " - "subsampling_factor=%d num_mels=%d (only 8/128 implemented)\n", + "subsampling_factor=%d num_mels=%d (only 8/128 implemented)", hp.enc_subsampling_factor, hp.fe_num_mels); return eb; } @@ -219,7 +220,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, 16384, false); if (eb.graph == nullptr) { - std::fprintf(stderr, "canary encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/canary/model.cpp b/src/arch/canary/model.cpp index 1a90ef26..ddeb81db 100644 --- a/src/arch/canary/model.cpp +++ b/src/arch/canary/model.cpp @@ -73,7 +73,7 @@ bool kv_cache_init(CanaryKvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, "canary kv_cache: unsupported kv_type=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary kv_cache: unsupported kv_type=%d", static_cast(kv_type)); return false; } @@ -87,7 +87,7 @@ bool kv_cache_init(CanaryKvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "canary kv_cache: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary kv_cache: ggml_init failed"); return false; } @@ -106,7 +106,7 @@ bool kv_cache_init(CanaryKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "canary kv_cache: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary kv_cache: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -123,9 +123,9 @@ bool kv_cache_init(CanaryKvCache & cache, const size_t total_bytes = ggml_nbytes(cache.self_k) + ggml_nbytes(cache.self_v) + ggml_nbytes(cache.cross_k) + ggml_nbytes(cache.cross_v); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "canary kv_cache: allocated %.1f MB (%s) " - "(self: %d session x %d layers, cross: %d T_enc x %d layers)\n", + "(self: %d session x %d layers, cross: %d T_enc x %d layers)", static_cast(total_bytes) / (1024.0 * 1024.0), ggml_type_name(kv_type), n_ctx, n_layer, T_enc, n_layer); @@ -149,7 +149,7 @@ bool kv_cache_init_batched(CanaryKvCache & cache, return true; } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, "canary kv_cache(batched): unsupported kv_type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary kv_cache(batched): unsupported kv_type"); return false; } const size_t ctx_size = 4 * ggml_tensor_overhead() + 256; @@ -462,8 +462,8 @@ transcribe_status load( { const int tok_vocab = m->tok.n_tokens(); if (tok_vocab > 0 && tok_vocab != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "canary: tokenizer vocab (%d) != stt.canary.decoder.vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: tokenizer vocab (%d) != stt.canary.decoder.vocab_size (%d)", tok_vocab, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } @@ -476,9 +476,9 @@ transcribe_status load( m->hparams.eos_token_id = m->hparams.endoftext_id; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: GGUF tokenizer has no eos_token_id and no " - "stt.canary.special.endoftext_id — regenerate GGUF\n"); + "stt.canary.special.endoftext_id — regenerate GGUF"); return TRANSCRIBE_ERR_GGUF; } @@ -561,7 +561,7 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, "canary: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -773,7 +773,7 @@ transcribe_status run( // ----- Mel front-end ------------------------------------------- if (!cm->mel.has_value()) { - std::fprintf(stderr, "canary run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } const int64_t t_mel_start = ggml_time_us(); @@ -784,7 +784,7 @@ transcribe_status run( cc->mel_buf, mel_n_mels, mel_n_frames); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, "canary run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -854,7 +854,7 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, "canary run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -922,7 +922,7 @@ transcribe_status run( if (const ggml_status gs = ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "canary run: encoder compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: encoder compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -958,8 +958,8 @@ transcribe_status run( const int d_dec = static_cast(eb.out->ne[0]); const int T_enc = static_cast(eb.out->ne[1]); if (d_dec <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "canary run: encoder output has degenerate shape [%d, %d]\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary run: encoder output has degenerate shape [%d, %d]", d_dec, T_enc); return TRANSCRIBE_ERR_GGUF; } @@ -981,16 +981,16 @@ transcribe_status run( const int src_id = find_language_id(cm->hparams, lang); const int tgt_id = find_language_id(cm->hparams, tgt_lang); if (src_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: source language '%s' has no token id " - "(model supports %zu langs)\n", + "(model supports %zu langs)", lang, cm->hparams.languages.size()); return TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE; } if (tgt_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: target language '%s' has no token id " - "(model supports %zu langs)\n", + "(model supports %zu langs)", tgt_lang, cm->hparams.languages.size()); return TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE; } @@ -1019,8 +1019,8 @@ transcribe_status run( task, pnc); } if (prompt_ids.empty()) { - std::fprintf(stderr, - "canary run: failed to build prompt for format='%s'\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary run: failed to build prompt for format='%s'", cm->hparams.prompt_format.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1101,7 +1101,7 @@ transcribe_status run( DecoderBuild cross_db = build_cross_kv_graph( cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, T_enc); if (cross_db.graph == nullptr) { - std::fprintf(stderr, "canary run: build_cross_kv_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: build_cross_kv_graph failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1120,7 +1120,7 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, cross_db.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "canary run: cross_kv compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: cross_kv compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1146,7 +1146,7 @@ transcribe_status run( /*skip_log_softmax=*/prompt_skip_softmax, cc->decoder_use_flash); if (db.out == nullptr || db.graph == nullptr) { - std::fprintf(stderr, "canary run: build_decoder_graph_kv (prompt) failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: build_decoder_graph_kv (prompt) failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1186,7 +1186,7 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, db.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "canary run: decoder prompt compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: decoder prompt compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1338,7 +1338,7 @@ transcribe_status run( cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, max_n_kv, T_enc, cc->decoder_use_flash); if (sb.graph == nullptr || sb.argmax_out == nullptr) { - std::fprintf(stderr, "canary run: build_step_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary run: build_step_graph failed"); commit_result(); return TRANSCRIBE_ERR_GGUF; } @@ -1891,9 +1891,9 @@ transcribe_status run_batch( } if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "canary run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n" - " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)\n", + " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)", n, T_enc_max, max_n_kv, prompt_len, mel_us / 1000.0, enc_us / 1000.0, n, dec_us / 1000.0); } diff --git a/src/arch/canary/weights.cpp b/src/arch/canary/weights.cpp index 8beb54b9..530abcae 100644 --- a/src/arch/canary/weights.cpp +++ b/src/arch/canary/weights.cpp @@ -16,6 +16,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -77,10 +78,10 @@ transcribe_status read_canary_hparams(const gguf_context * gguf, const auto r = read_token_id_required(gguf, key, out); if (r == KvResult::Ok) return TRANSCRIBE_OK; if (r == KvResult::Absent) { - std::fprintf(stderr, "%s: required special-token KV missing: %s\n", kFamilyTag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: required special-token KV missing: %s", kFamilyTag, key); return TRANSCRIBE_ERR_GGUF; } - std::fprintf(stderr, "%s: special-token KV %s has wrong type\n", kFamilyTag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: special-token KV %s has wrong type", kFamilyTag, key); return TRANSCRIBE_ERR_GGUF; }; @@ -92,7 +93,7 @@ transcribe_status read_canary_hparams(const gguf_context * gguf, auto opt_special = [&](const char * key, int32_t & out) -> transcribe_status { const auto r = read_token_id_required(gguf, key, out); if (r == KvResult::Ok || r == KvResult::Absent) return TRANSCRIBE_OK; - std::fprintf(stderr, "%s: optional special-token KV %s has wrong type\n", kFamilyTag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: optional special-token KV %s has wrong type", kFamilyTag, key); return TRANSCRIBE_ERR_GGUF; }; @@ -153,7 +154,7 @@ transcribe_status read_canary_hparams(const gguf_context * gguf, hp.languages.push_back(code); hp.language_ids.push_back(v); } else if (kr == KvResult::BadType) { - std::fprintf(stderr, "%s: %s wrong type\n", kFamilyTag, key.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: %s wrong type", kFamilyTag, key.c_str()); return TRANSCRIBE_ERR_GGUF; } } @@ -179,70 +180,70 @@ transcribe_status read_canary_hparams(const gguf_context * gguf, hp.enc_d_ff <= 0 || hp.enc_conv_kernel <= 0 || hp.enc_subsampling_factor <= 0 || hp.enc_subsampling_channels <= 0) { - std::fprintf(stderr, "canary: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "canary: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_d_model <= 0 || hp.dec_n_heads <= 0 || hp.dec_d_ff <= 0 || hp.dec_max_position <= 0 || hp.dec_vocab_size <= 0) { - std::fprintf(stderr, "canary: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_d_model % hp.dec_n_heads != 0) { - std::fprintf(stderr, - "canary: decoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: decoder d_model (%d) not divisible by n_heads (%d)", hp.dec_d_model, hp.dec_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_activation != "relu" && hp.dec_activation != "silu" && hp.dec_activation != "swish") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: unsupported decoder activation \"%s\" " - "(only relu, silu, swish are implemented)\n", + "(only relu, silu, swish are implemented)", hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, - "canary: unsupported frontend type \"%s\" (only \"mel\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: unsupported frontend type \"%s\" (only \"mel\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_window != "hann") { - std::fprintf(stderr, - "canary: unsupported frontend window \"%s\" (only \"hann\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: unsupported frontend window \"%s\" (only \"hann\")", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_normalize != "per_feature") { - std::fprintf(stderr, - "canary: unsupported frontend normalize \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: unsupported frontend normalize \"%s\"", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if ((hp.fe_n_fft & (hp.fe_n_fft - 1)) != 0) { - std::fprintf(stderr, - "canary: frontend n_fft (%d) must be a power of 2\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: frontend n_fft (%d) must be a power of 2", hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_pad_mode != "reflect" && hp.fe_pad_mode != "constant") { - std::fprintf(stderr, - "canary: unsupported frontend pad_mode \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: unsupported frontend pad_mode \"%s\"", hp.fe_pad_mode.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.prompt_format != "canary" && hp.prompt_format != "canary2") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: unsupported prompt_format \"%s\" " - "(only \"canary\" and \"canary2\")\n", + "(only \"canary\" and \"canary2\")", hp.prompt_format.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -397,12 +398,12 @@ transcribe_status build_canary_weights(ggml_context * ctx_meta, { ggml_tensor * tw = ggml_get_tensor(ctx_meta, "dec.embed.token.weight"); if (tw == nullptr) { - std::fprintf(stderr, "canary: missing tensor \"dec.embed.token.weight\"\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary: missing tensor \"dec.embed.token.weight\""); return TRANSCRIBE_ERR_GGUF; } if (tw->ne[0] != d_dec || tw->ne[1] != vocab) { - std::fprintf(stderr, - "canary: dec.embed.token.weight shape [%lld,%lld] expected [%lld,%lld]\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary: dec.embed.token.weight shape [%lld,%lld] expected [%lld,%lld]", (long long)tw->ne[0], (long long)tw->ne[1], (long long)d_dec, (long long)vocab); return TRANSCRIBE_ERR_GGUF; diff --git a/src/arch/canary_qwen/decoder.cpp b/src/arch/canary_qwen/decoder.cpp index f9e516f5..79ca07e6 100644 --- a/src/arch/canary_qwen/decoder.cpp +++ b/src/arch/canary_qwen/decoder.cpp @@ -15,6 +15,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -76,27 +77,27 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, pb.suffix_len = suffix_len; if (ctx == nullptr || T_prompt <= 0 || T_audio < 0) { - std::fprintf(stderr, - "canary_qwen decoder: invalid arg (T_prompt=%d, T_audio=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen decoder: invalid arg (T_prompt=%d, T_audio=%d)", T_prompt, T_audio); return pb; } if (prefix_len < 0 || suffix_len < 0 || prefix_len + T_audio + suffix_len != T_prompt) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen decoder: prefix_len(%d) + T_audio(%d) + " - "suffix_len(%d) != T_prompt(%d)\n", + "suffix_len(%d) != T_prompt(%d)", prefix_len, T_audio, suffix_len, T_prompt); return pb; } if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { - std::fprintf(stderr, "canary_qwen decoder: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen decoder: kv_cache not initialized"); return pb; } if (T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, - "canary_qwen decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d", T_prompt, kv_cache.n_ctx); return pb; } @@ -128,8 +129,8 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "canary_qwen decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen decoder: ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -274,17 +275,17 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0) { - std::fprintf(stderr, - "canary_qwen step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, "canary_qwen step: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen step: kv_cache not initialized"); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "canary_qwen step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -313,8 +314,8 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "canary_qwen step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen step: ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -378,7 +379,7 @@ PrefillBuildBatched build_prefill_graph_batched( n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt_max > kv_cache.n_ctx) { - std::fprintf(stderr, "canary_qwen prefill(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen prefill(batched): invalid arg"); return pb; } @@ -461,7 +462,7 @@ StepBuildBatched build_step_graph_batched( if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "canary_qwen step(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen step(batched): invalid arg"); return sb; } diff --git a/src/arch/canary_qwen/encoder.cpp b/src/arch/canary_qwen/encoder.cpp index 4eac4c2e..ff0cc785 100644 --- a/src/arch/canary_qwen/encoder.cpp +++ b/src/arch/canary_qwen/encoder.cpp @@ -17,6 +17,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -101,16 +102,16 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, - "canary_qwen encoder: invalid arg (ctx=%p, n_mel_frames=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen encoder: invalid arg (ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } if (hp.enc_subsampling_factor != 8 || hp.fe_num_mels != 128) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen encoder: unsupported geometry " - "subsampling_factor=%d num_mels=%d\n", + "subsampling_factor=%d num_mels=%d", hp.enc_subsampling_factor, hp.fe_num_mels); return eb; } @@ -214,7 +215,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, 16384, false); if (eb.graph == nullptr) { - std::fprintf(stderr, "canary_qwen encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/canary_qwen/model.cpp b/src/arch/canary_qwen/model.cpp index 209aec3f..07b1d166 100644 --- a/src/arch/canary_qwen/model.cpp +++ b/src/arch/canary_qwen/model.cpp @@ -219,8 +219,8 @@ transcribe_status resolve_chat_tokens(const transcribe::Tokenizer & tok, for (const auto & p : pieces) { const int id = tok.find(p.piece); if (id < 0) { - std::fprintf(stderr, - "canary_qwen: chat-template piece \"%s\" not in tokenizer\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: chat-template piece \"%s\" not in tokenizer", p.piece); return TRANSCRIBE_ERR_GGUF; } @@ -231,9 +231,9 @@ transcribe_status resolve_chat_tokens(const transcribe::Tokenizer & tok, transcribe_status build_static_prompt_segments(CanaryQwenModel & m) { if (!m.tok.has_encoder()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen: tokenizer has no BPE encoder — cannot " - "tokenize chat template at load time\n"); + "tokenize chat template at load time"); return TRANSCRIBE_ERR_GGUF; } @@ -245,7 +245,7 @@ transcribe_status build_static_prompt_segments(CanaryQwenModel & m) { if (auto st = m.tok.encode("user\nTranscribe the following: ", ids); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "canary_qwen: prefix BPE encode failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen: prefix BPE encode failed"); return st; } m.prompt_prefix_ids.insert(m.prompt_prefix_ids.end(), @@ -387,8 +387,8 @@ transcribe_status promote_conv_pw_to_f32_on_cpu(CanaryQwenModel & m) { m.conv_pw_f32_buffer = ggml_backend_alloc_ctx_tensors( m.conv_pw_f32_ctx, m.plan.primary); if (m.conv_pw_f32_buffer == nullptr) { - std::fprintf(stderr, - "canary_qwen: conv F16->F32 promotion buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: conv F16->F32 promotion buffer alloc failed"); ggml_free(m.conv_pw_f32_ctx); m.conv_pw_f32_ctx = nullptr; return TRANSCRIBE_ERR_BACKEND; @@ -398,8 +398,8 @@ transcribe_status promote_conv_pw_to_f32_on_cpu(CanaryQwenModel & m) { const auto * f16_traits = ggml_get_type_traits(GGML_TYPE_F16); if (f16_traits == nullptr || f16_traits->to_float == nullptr) { - std::fprintf(stderr, - "canary_qwen: no f16 to_float trait — skipping conv promotion\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "canary_qwen: no f16 to_float trait — skipping conv promotion"); ggml_backend_buffer_free(m.conv_pw_f32_buffer); m.conv_pw_f32_buffer = nullptr; ggml_free(m.conv_pw_f32_ctx); @@ -428,8 +428,8 @@ transcribe_status promote_conv_pw_to_f32_on_cpu(CanaryQwenModel & m) { *slots[i].dst_slot = dst; } - std::fprintf(stderr, - "canary_qwen: promoted %zu F16 conv weights to F32\n", slots.size()); + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, + "canary_qwen: promoted %zu F16 conv weights to F32", slots.size()); return TRANSCRIBE_OK; } @@ -518,8 +518,8 @@ transcribe_status promote_linears_bf16_to_f32_on_cpu(CanaryQwenModel & m) { m.linear_f32_buffer = ggml_backend_alloc_ctx_tensors( m.linear_f32_ctx, m.plan.primary); if (m.linear_f32_buffer == nullptr) { - std::fprintf(stderr, - "canary_qwen: BF16→F32 linear promotion buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: BF16→F32 linear promotion buffer alloc failed"); ggml_free(m.linear_f32_ctx); m.linear_f32_ctx = nullptr; return TRANSCRIBE_ERR_BACKEND; @@ -529,8 +529,8 @@ transcribe_status promote_linears_bf16_to_f32_on_cpu(CanaryQwenModel & m) { const auto * bf16_traits = ggml_get_type_traits(GGML_TYPE_BF16); if (bf16_traits == nullptr || bf16_traits->to_float == nullptr) { - std::fprintf(stderr, - "canary_qwen: no bf16 to_float trait — skipping linear promotion\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "canary_qwen: no bf16 to_float trait — skipping linear promotion"); ggml_backend_buffer_free(m.linear_f32_buffer); m.linear_f32_buffer = nullptr; ggml_free(m.linear_f32_ctx); @@ -559,8 +559,8 @@ transcribe_status promote_linears_bf16_to_f32_on_cpu(CanaryQwenModel & m) { *slots[i].dst_slot = dst; } - std::fprintf(stderr, - "canary_qwen: promoted %zu BF16 linear weights to F32 for CPU backend\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, + "canary_qwen: promoted %zu BF16 linear weights to F32 for CPU backend", slots.size()); return TRANSCRIBE_OK; } @@ -634,14 +634,14 @@ transcribe_status load( m->hparams.eos_token_id = m->tok.eos_id(); if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "canary_qwen: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, - "canary_qwen: GGUF tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: GGUF tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } @@ -719,8 +719,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "canary_qwen: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -908,8 +908,8 @@ transcribe_status run(transcribe_session * context, cc->n_threads); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "canary_qwen run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -927,7 +927,7 @@ transcribe_status run(transcribe_session * context, ip.no_alloc = true; cc->compute_ctx = ggml_init(ip); if (cc->compute_ctx == nullptr) { - std::fprintf(stderr, "canary_qwen run: ggml_init failed (encoder)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "canary_qwen run: ggml_init failed (encoder)"); return TRANSCRIBE_ERR_GGUF; } } @@ -944,8 +944,8 @@ transcribe_status run(transcribe_session * context, static_cast(cm->plan.scheduler_list.size()), 16384, /*parallel=*/false, /*op_offload=*/true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "canary_qwen run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -988,8 +988,8 @@ transcribe_status run(transcribe_session * context, ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "canary_qwen run: encoder compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen run: encoder compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1167,8 +1167,8 @@ transcribe_status run(transcribe_session * context, ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "canary_qwen run: prefill compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen run: prefill compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1251,12 +1251,12 @@ transcribe_status run(transcribe_session * context, ggml_tensor * t = ggml_graph_node(sb.graph, ni); if (t != nullptr) op_counts[t->op] += 1; } - std::fprintf(stderr, "[profile_decode] step graph: n_nodes=%d\n", n_nodes); + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[profile_decode] step graph: n_nodes=%d", n_nodes); int shown = 0; for (int o = 0; o < GGML_OP_COUNT && shown < 12; ++o) { if (op_counts[o] > 0) { - std::fprintf(stderr, - "[profile_decode] op %-22s x %4d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] op %-22s x %4d", ggml_op_name(static_cast(o)), op_counts[o]); ++shown; } @@ -1316,8 +1316,8 @@ transcribe_status run(transcribe_session * context, ggml_backend_sched_graph_compute(cc->sched, sb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "canary_qwen run: step compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "canary_qwen run: step compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1358,14 +1358,14 @@ transcribe_status run(transcribe_session * context, if (profile_decode) { const int n = n_steps; - std::fprintf(stderr, - "[profile_decode] T_prompt=%d max_n_kv=%d steps=%d use_flash=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] T_prompt=%d max_n_kv=%d steps=%d use_flash=%d", T_prompt, max_n_kv, n, cc->decoder_use_flash ? 1 : 0); - std::fprintf(stderr, - "[profile_decode] prefill_compute=%.2f ms\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] prefill_compute=%.2f ms", t_prefill_compute_us / 1000.0); - std::fprintf(stderr, - "[profile_decode] step totals: input_set=%.2f ms compute=%.2f ms argmax_get=%.2f ms\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] step totals: input_set=%.2f ms compute=%.2f ms argmax_get=%.2f ms", t_step_input_set_us / 1000.0, t_step_compute_us / 1000.0, t_step_argmax_get_us / 1000.0); if (n > 0) { @@ -1376,16 +1376,16 @@ transcribe_status run(transcribe_session * context, if (idx >= sorted.size()) idx = sorted.size() - 1; return sorted[idx] / 1000.0; }; - std::fprintf(stderr, - "[profile_decode] per-step compute ms: mean=%.2f p50=%.2f p90=%.2f p99=%.2f min=%.2f max=%.2f\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] per-step compute ms: mean=%.2f p50=%.2f p90=%.2f p99=%.2f min=%.2f max=%.2f", (t_step_compute_us / 1000.0) / n, pct(0.50), pct(0.90), pct(0.99), sorted.front() / 1000.0, sorted.back() / 1000.0); - std::fprintf(stderr, - "[profile_decode] per-step input_set ms: mean=%.3f\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] per-step input_set ms: mean=%.3f", (t_step_input_set_us / 1000.0) / n); - std::fprintf(stderr, - "[profile_decode] per-step argmax_get ms: mean=%.3f\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[profile_decode] per-step argmax_get ms: mean=%.3f", (t_step_argmax_get_us / 1000.0) / n); } } diff --git a/src/arch/canary_qwen/weights.cpp b/src/arch/canary_qwen/weights.cpp index 12ae7056..c887edb0 100644 --- a/src/arch/canary_qwen/weights.cpp +++ b/src/arch/canary_qwen/weights.cpp @@ -11,6 +11,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -67,8 +68,8 @@ transcribe_status read_canary_qwen_hparams(gguf_context * gguf, if (r == KvResult::Ok) { hp.audio_locator_id = v; } else { - std::fprintf(stderr, - "%s: missing or invalid stt.canary_qwen.perception.audio_locator_id\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: missing or invalid stt.canary_qwen.perception.audio_locator_id", kFamilyTag); return TRANSCRIBE_ERR_GGUF; } @@ -127,29 +128,29 @@ transcribe_status read_canary_qwen_hparams(gguf_context * gguf, // Cross-field validation. if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, "%s: dec.n_heads (%d) must be divisible by dec.n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: dec.n_heads (%d) must be divisible by dec.n_kv_heads (%d)", kFamilyTag, hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads * hp.dec_head_dim != hp.dec_hidden) { - std::fprintf(stderr, "%s: dec.n_heads(%d) * dec.head_dim(%d) != dec.hidden(%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: dec.n_heads(%d) * dec.head_dim(%d) != dec.hidden(%d)", kFamilyTag, hp.dec_n_heads, hp.dec_head_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.perception_output_dim != hp.dec_hidden) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: perception.output_dim(%d) must equal dec.hidden(%d) " - "for direct concat into LM embedding stream\n", + "for direct concat into LM embedding stream", kFamilyTag, hp.perception_output_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.audio_locator_id < 0) { - std::fprintf(stderr, "%s: audio_locator_id must be >= 0\n", kFamilyTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: audio_locator_id must be >= 0", kFamilyTag); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_subsampling_factor != 8 || hp.fe_num_mels != 128) { - std::fprintf(stderr, - "%s: only subsampling_factor=8 num_mels=128 supported (got %d, %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: only subsampling_factor=8 num_mels=128 supported (got %d, %d)", kFamilyTag, hp.enc_subsampling_factor, hp.fe_num_mels); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/cohere/decoder.cpp b/src/arch/cohere/decoder.cpp index f50d15cd..01eec367 100644 --- a/src/arch/cohere/decoder.cpp +++ b/src/arch/cohere/decoder.cpp @@ -31,6 +31,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -462,9 +463,9 @@ DecoderBuild build_decoder_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || seq_len <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere decoder: invalid arg " - "(ctx=%p, seq_len=%d, T_enc=%d)\n", + "(ctx=%p, seq_len=%d, T_enc=%d)", static_cast(ctx), seq_len, T_enc); return db; } @@ -599,8 +600,8 @@ DecoderBuild build_decoder_graph(ggml_context * ctx, // Build the forward graph. db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "cohere decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere decoder: ggml_new_graph_custom failed"); return db; } ggml_build_forward_expand(db.graph, db.out); @@ -621,8 +622,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || T_enc <= 0) { - std::fprintf(stderr, - "cohere cross_kv: invalid arg (ctx=%p, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere cross_kv: invalid arg (ctx=%p, T_enc=%d)", static_cast(ctx), T_enc); return db; } @@ -636,8 +637,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 4096, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "cohere cross_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere cross_kv: ggml_new_graph_custom failed"); return db; } @@ -695,9 +696,9 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || n_tokens <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere decoder_kv: invalid arg " - "(ctx=%p, n_tokens=%d, T_enc=%d)\n", + "(ctx=%p, n_tokens=%d, T_enc=%d)", static_cast(ctx), n_tokens, T_enc); return db; } @@ -760,8 +761,8 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, // the self-attention cache writes). db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "cohere decoder_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere decoder_kv: ggml_new_graph_custom failed"); return db; } @@ -894,14 +895,14 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "cohere step: invalid arg (max_n_kv=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere step: invalid arg (max_n_kv=%d, T_enc=%d)", max_n_kv, T_enc); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "cohere step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -927,7 +928,7 @@ StepBuild build_step_graph(ggml_context * ctx, sb.graph = ggml_new_graph_custom(ctx, 8192, false); if (sb.graph == nullptr) { - std::fprintf(stderr, "cohere step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere step: ggml_new_graph_custom failed"); return sb; } @@ -1172,7 +1173,7 @@ StepBuildBatched build_step_graph_batched(ggml_context * ctx, sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || T_enc_max <= 0 || n_batch <= 0) return sb; if (!use_flash) { - std::fprintf(stderr, "cohere step(batched): requires flash path\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere step(batched): requires flash path"); return sb; } diff --git a/src/arch/cohere/encoder.cpp b/src/arch/cohere/encoder.cpp index b29c3563..5e03a66d 100644 --- a/src/arch/cohere/encoder.cpp +++ b/src/arch/cohere/encoder.cpp @@ -20,6 +20,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -125,18 +126,18 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere encoder: invalid arg " - "(ctx=%p, n_mel_frames=%d)\n", + "(ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } if (hp.enc_subsampling_factor != 8 || hp.fe_num_mels != 128) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere encoder: unsupported geometry " "subsampling_factor=%d num_mels=%d " - "(only 8/128 implemented)\n", + "(only 8/128 implemented)", hp.enc_subsampling_factor, hp.fe_num_mels); return eb; } @@ -233,8 +234,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // large graph; 16384 leaves headroom. eb.graph = ggml_new_graph_custom(ctx, 16384, false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "cohere encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/cohere/model.cpp b/src/arch/cohere/model.cpp index f075a367..b6c95b68 100644 --- a/src/arch/cohere/model.cpp +++ b/src/arch/cohere/model.cpp @@ -74,9 +74,9 @@ bool kv_cache_init(CohereKvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere kv_cache: unsupported kv_type=%d " - "(only F16/F32)\n", static_cast(kv_type)); + "(only F16/F32)", static_cast(kv_type)); return false; } @@ -90,7 +90,7 @@ bool kv_cache_init(CohereKvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "cohere kv_cache: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere kv_cache: ggml_init failed"); return false; } @@ -109,7 +109,7 @@ bool kv_cache_init(CohereKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "cohere kv_cache: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere kv_cache: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -127,9 +127,9 @@ bool kv_cache_init(CohereKvCache & cache, const size_t total_bytes = ggml_nbytes(cache.self_k) + ggml_nbytes(cache.self_v) + ggml_nbytes(cache.cross_k) + ggml_nbytes(cache.cross_v); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "cohere kv_cache: allocated %.1f MB (%s) " - "(self: %d session x %d layers, cross: %d T_enc x %d layers)\n", + "(self: %d session x %d layers, cross: %d T_enc x %d layers)", static_cast(total_bytes) / (1024.0 * 1024.0), ggml_type_name(kv_type), n_ctx, n_layer, T_enc, n_layer); @@ -153,7 +153,7 @@ bool kv_cache_init_batched(CohereKvCache & cache, return true; } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, "cohere kv_cache(batched): unsupported kv_type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere kv_cache(batched): unsupported kv_type"); return false; } @@ -381,9 +381,9 @@ transcribe_status fuse_encoder_q_bias(CohereModel & m) { const int64_t head_dim = n_heads > 0 ? d_model / n_heads : 0; if (head_dim * n_heads != d_model) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: d_model (%lld) != head_dim*n_heads; " - "skipping encoder Q-bias fusion\n", (long long)d_model); + "skipping encoder Q-bias fusion", (long long)d_model); return TRANSCRIBE_OK; } @@ -424,8 +424,8 @@ transcribe_status fuse_encoder_q_bias(CohereModel & m) { } if (fused > 0) { - std::fprintf(stderr, - "cohere: fused Q bias into pos_u/pos_v for %zu encoder blocks\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, + "cohere: fused Q bias into pos_u/pos_v for %zu encoder blocks", fused); } return TRANSCRIBE_OK; @@ -590,9 +590,9 @@ transcribe_status load( // missing EOS is a GGUF-builder bug that should surface during // conversion, not hide behind a runtime fallback in production. if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: GGUF tokenizer has no eos_token_id -- " - "regenerate with an up-to-date converter\n"); + "regenerate with an up-to-date converter"); return TRANSCRIBE_ERR_GGUF; } @@ -687,8 +687,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "cohere: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -805,8 +805,8 @@ transcribe_status run( // ----- Mel front-end ------------------------------------------- if (!cm->mel.has_value()) { - std::fprintf(stderr, - "cohere run: model has no MelFrontend (load skipped?)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: model has no MelFrontend (load skipped?)"); return TRANSCRIBE_ERR_INVALID_ARG; } const int64_t t_mel_start = ggml_time_us(); @@ -817,8 +817,8 @@ transcribe_status run( cc->mel_buf, mel_n_mels, mel_n_frames); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "cohere run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -890,8 +890,8 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "cohere run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -968,8 +968,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "cohere run: encoder graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: encoder graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1006,9 +1006,9 @@ transcribe_status run( const int d_enc = static_cast(eb.out->ne[0]); const int T_enc = static_cast(eb.out->ne[1]); if (d_enc <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere run: encoder output has degenerate shape " - "[%d, %d]\n", d_enc, T_enc); + "[%d, %d]", d_enc, T_enc); return TRANSCRIBE_ERR_GGUF; } cc->enc_host.resize(static_cast(d_enc) * @@ -1063,8 +1063,8 @@ transcribe_status run( for (const auto & piece : prompt_pieces) { const int id = cm->tok.find(piece); if (id < 0) { - std::fprintf(stderr, - "cohere run: unknown prompt token '%s'\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: unknown prompt token '%s'", piece.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1160,8 +1160,8 @@ transcribe_status run( cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, T_enc); if (cross_db.graph == nullptr) { - std::fprintf(stderr, - "cohere run: build_cross_kv_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: build_cross_kv_graph failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1181,8 +1181,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, cross_db.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "cohere run: cross_kv compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: cross_kv compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1208,8 +1208,8 @@ transcribe_status run( /*skip_log_softmax=*/prompt_skip_softmax, cc->decoder_use_flash); if (db.out == nullptr || db.graph == nullptr) { - std::fprintf(stderr, - "cohere run: build_decoder_graph_kv (prompt) failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: build_decoder_graph_kv (prompt) failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1250,8 +1250,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, db.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "cohere run: decoder prompt compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: decoder prompt compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1397,8 +1397,8 @@ transcribe_status run( cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, max_n_kv, T_enc, cc->decoder_use_flash); if (sb.graph == nullptr || sb.argmax_out == nullptr) { - std::fprintf(stderr, - "cohere run: build_step_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: build_step_graph failed"); commit_result(); return TRANSCRIBE_ERR_GGUF; } @@ -1456,8 +1456,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, sb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "cohere run: step compute failed (%d, n_past=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere run: step compute failed (%d, n_past=%d)", static_cast(gs), n_past); commit_result(); return TRANSCRIBE_ERR_GGUF; @@ -2001,9 +2001,9 @@ transcribe_status run_batch( } if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "cohere run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n" - " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)\n", + " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)", n, T_enc_max, max_n_kv, prompt_len, mel_us / 1000.0, enc_us / 1000.0, n, dec_us / 1000.0); } diff --git a/src/arch/cohere/weights.cpp b/src/arch/cohere/weights.cpp index def553f9..27ee6be6 100644 --- a/src/arch/cohere/weights.cpp +++ b/src/arch/cohere/weights.cpp @@ -11,6 +11,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -84,84 +85,84 @@ transcribe_status read_cohere_hparams(const gguf_context * gguf, hp.enc_d_ff <= 0 || hp.enc_conv_kernel <= 0 || hp.enc_subsampling_factor <= 0 || hp.enc_subsampling_channels <= 0) { - std::fprintf(stderr, "cohere: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "cohere: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_inner <= 0 || hp.dec_max_seq <= 0) { - std::fprintf(stderr, "cohere: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden % hp.dec_n_heads != 0) { - std::fprintf(stderr, - "cohere: decoder hidden (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: decoder hidden (%d) not divisible by n_heads (%d)", hp.dec_hidden, hp.dec_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_activation != "relu" && hp.dec_activation != "silu" && hp.dec_activation != "swish") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: unsupported decoder activation \"%s\" " - "(only relu, silu, swish are implemented)\n", + "(only relu, silu, swish are implemented)", hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_num_mels <= 0 || hp.fe_sample_rate <= 0 || hp.fe_n_fft <= 0 || hp.fe_win_length <= 0 || hp.fe_hop_length <= 0) { - std::fprintf(stderr, "cohere: frontend dimensions must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: frontend dimensions must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_win_length > hp.fe_n_fft) { - std::fprintf(stderr, - "cohere: frontend win_length (%d) > n_fft (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: frontend win_length (%d) > n_fft (%d)", hp.fe_win_length, hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_f_min < 0.0f || hp.fe_f_max <= hp.fe_f_min) { - std::fprintf(stderr, - "cohere: frontend mel band invalid: f_min=%f f_max=%f\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: frontend mel band invalid: f_min=%f f_max=%f", hp.fe_f_min, hp.fe_f_max); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, - "cohere: unsupported frontend type \"%s\" (only \"mel\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: unsupported frontend type \"%s\" (only \"mel\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_window != "hann") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: unsupported frontend window \"%s\" " - "(only \"hann\" is implemented)\n", + "(only \"hann\" is implemented)", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_normalize != "per_feature") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: unsupported frontend normalize \"%s\" " - "(only \"per_feature\" is implemented)\n", + "(only \"per_feature\" is implemented)", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if ((hp.fe_n_fft & (hp.fe_n_fft - 1)) != 0) { - std::fprintf(stderr, - "cohere: frontend n_fft (%d) must be a power of 2\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: frontend n_fft (%d) must be a power of 2", hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_pad_mode != "reflect" && hp.fe_pad_mode != "constant") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: unsupported frontend pad_mode \"%s\" " - "(only \"reflect\" and \"constant\" are implemented)\n", + "(only \"reflect\" and \"constant\" are implemented)", hp.fe_pad_mode.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -320,13 +321,13 @@ transcribe_status build_cohere_weights(ggml_context * ctx_meta, { ggml_tensor * tw = ggml_get_tensor(ctx_meta, "dec.embed.token.weight"); if (tw == nullptr) { - std::fprintf(stderr, "cohere: missing tensor \"dec.embed.token.weight\"\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "cohere: missing tensor \"dec.embed.token.weight\""); return TRANSCRIBE_ERR_GGUF; } // The embedding table: ne[0]=dec_hidden, ne[1]=vocab_size. if (tw->ne[0] != dec_h) { - std::fprintf(stderr, - "cohere: dec.embed.token.weight ne[0]=%lld, expected %lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "cohere: dec.embed.token.weight ne[0]=%lld, expected %lld", static_cast(tw->ne[0]), static_cast(dec_h)); return TRANSCRIBE_ERR_GGUF; diff --git a/src/arch/funasr_nano/adaptor.cpp b/src/arch/funasr_nano/adaptor.cpp index b72628d4..0dce8f94 100644 --- a/src/arch/funasr_nano/adaptor.cpp +++ b/src/arch/funasr_nano/adaptor.cpp @@ -5,6 +5,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -145,8 +146,8 @@ AdaptorBuild build_adaptor_graph(ggml_context * ctx, { AdaptorBuild ab {}; if (ctx == nullptr || T_in <= 0) { - std::fprintf(stderr, - "funasr_nano adaptor: invalid arg (T_in=%d)\n", T_in); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano adaptor: invalid arg (T_in=%d)", T_in); return ab; } @@ -189,8 +190,8 @@ AdaptorBuild build_adaptor_graph(ggml_context * ctx, ab.graph = ggml_new_graph_custom(ctx, /*size=*/2048, /*grads=*/false); if (ab.graph == nullptr) { - std::fprintf(stderr, - "funasr_nano adaptor: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano adaptor: ggml_new_graph_custom failed"); return ab; } ggml_build_forward_expand(ab.graph, ab.out); diff --git a/src/arch/funasr_nano/decoder.cpp b/src/arch/funasr_nano/decoder.cpp index 40f89e5f..d0a56392 100644 --- a/src/arch/funasr_nano/decoder.cpp +++ b/src/arch/funasr_nano/decoder.cpp @@ -25,6 +25,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -86,26 +87,26 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, pb.suffix_len = suffix_len; if (ctx == nullptr || T_prompt <= 0 || T_audio < 0) { - std::fprintf(stderr, - "funasr_nano decoder: invalid arg (T_prompt=%d, T_audio=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano decoder: invalid arg (T_prompt=%d, T_audio=%d)", T_prompt, T_audio); return pb; } if (prefix_len < 0 || suffix_len < 0 || prefix_len + T_audio + suffix_len != T_prompt) { - std::fprintf(stderr, - "funasr_nano decoder: prefix_len(%d) + T_audio(%d) + suffix_len(%d) != T_prompt(%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano decoder: prefix_len(%d) + T_audio(%d) + suffix_len(%d) != T_prompt(%d)", prefix_len, T_audio, suffix_len, T_prompt); return pb; } if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { - std::fprintf(stderr, "funasr_nano decoder: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano decoder: kv_cache not initialized"); return pb; } if (T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, - "funasr_nano decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d", T_prompt, kv_cache.n_ctx); return pb; } @@ -138,8 +139,8 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "funasr_nano decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano decoder: ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -265,17 +266,17 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0) { - std::fprintf(stderr, - "funasr_nano step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, "funasr_nano step: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano step: kv_cache not initialized"); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "funasr_nano step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -304,8 +305,8 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "funasr_nano step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano step: ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -368,7 +369,7 @@ PrefillBuildBatched build_prefill_graph_batched( n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt_max > kv_cache.n_ctx) { - std::fprintf(stderr, "funasr_nano prefill(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano prefill(batched): invalid arg"); return pb; } @@ -451,7 +452,7 @@ StepBuildBatched build_step_graph_batched( if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "funasr_nano step(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano step(batched): invalid arg"); return sb; } diff --git a/src/arch/funasr_nano/encoder.cpp b/src/arch/funasr_nano/encoder.cpp index edfe1729..e27f7ae1 100644 --- a/src/arch/funasr_nano/encoder.cpp +++ b/src/arch/funasr_nano/encoder.cpp @@ -26,6 +26,7 @@ #include "conformer/conformer.h" #include "sanm/sanm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -74,9 +75,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, { EncoderBuild eb {}; if (ctx == nullptr || n_lfr_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano encoder: invalid arg " - "(ctx=%p, n_lfr_frames=%d)\n", + "(ctx=%p, n_lfr_frames=%d)", static_cast(ctx), n_lfr_frames); return eb; } @@ -163,7 +164,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "funasr_nano encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/funasr_nano/model.cpp b/src/arch/funasr_nano/model.cpp index 3d80f6d2..ad91b60d 100644 --- a/src/arch/funasr_nano/model.cpp +++ b/src/arch/funasr_nano/model.cpp @@ -162,8 +162,8 @@ transcribe_status resolve_chat_tokens(const transcribe::Tokenizer & tok, for (const auto & p : pieces) { const int id = tok.find(p.piece); if (id < 0) { - std::fprintf(stderr, - "funasr_nano: chat-template piece \"%s\" not in tokenizer\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: chat-template piece \"%s\" not in tokenizer", p.piece); return TRANSCRIBE_ERR_GGUF; } @@ -368,14 +368,14 @@ transcribe_status load( m->hparams.eos_token_id = m->tok.eos_id(); if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "funasr_nano: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, - "funasr_nano: GGUF tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: GGUF tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } @@ -411,8 +411,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "funasr_nano: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -579,8 +579,8 @@ transcribe_status run( ip.no_alloc = true; cc->compute_ctx = ggml_init(ip); if (cc->compute_ctx == nullptr) { - std::fprintf(stderr, - "funasr_nano run: ggml_init for compute_ctx failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano run: ggml_init for compute_ctx failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -598,7 +598,7 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 16384, /*parallel=*/false, /*op_offload=*/true); if (cc->sched == nullptr) { - std::fprintf(stderr, "funasr_nano run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -623,8 +623,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "funasr_nano run: encoder graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano run: encoder graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -693,8 +693,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, ab.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "funasr_nano run: adaptor graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano run: adaptor graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -870,8 +870,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "funasr_nano run: prefill graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano run: prefill graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -986,8 +986,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, sb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "funasr_nano run: step graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano run: step graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/funasr_nano/weights.cpp b/src/arch/funasr_nano/weights.cpp index e58af80d..30992608 100644 --- a/src/arch/funasr_nano/weights.cpp +++ b/src/arch/funasr_nano/weights.cpp @@ -9,6 +9,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -90,101 +91,101 @@ transcribe_status read_funasr_nano_hparams(const gguf_context * gguf, hp.enc_n_heads <= 0 || hp.enc_d_ff <= 0 || hp.enc_kernel <= 0 || hp.enc_kernel % 2 == 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: encoder hparams invalid (n_blocks=%d " "tp_blocks=%d d_model=%d d_input=%d n_heads=%d d_ff=%d " - "kernel=%d — kernel must be positive and odd)\n", + "kernel=%d — kernel must be positive and odd)", hp.enc_n_blocks, hp.enc_tp_blocks, hp.enc_d_model, hp.enc_d_input, hp.enc_n_heads, hp.enc_d_ff, hp.enc_kernel); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "funasr_nano: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_input != hp.fe_num_mels * hp.fe_lfr_m) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: encoder d_input (%d) must equal num_mels (%d) " - "× lfr_m (%d) = %d\n", + "× lfr_m (%d) = %d", hp.enc_d_input, hp.fe_num_mels, hp.fe_lfr_m, hp.fe_num_mels * hp.fe_lfr_m); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_attn_type != "sanm") { - std::fprintf(stderr, - "funasr_nano: unsupported encoder attention type \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: unsupported encoder attention type \"%s\"", hp.enc_attn_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.adaptor_n_heads <= 0 || hp.adaptor_d_head <= 0 || hp.adaptor_llm_dim != hp.adaptor_n_heads * hp.adaptor_d_head) { - std::fprintf(stderr, - "funasr_nano: adaptor llm_dim (%d) != n_heads (%d) * d_head (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: adaptor llm_dim (%d) != n_heads (%d) * d_head (%d)", hp.adaptor_llm_dim, hp.adaptor_n_heads, hp.adaptor_d_head); return TRANSCRIBE_ERR_GGUF; } if (hp.adaptor_activation != "relu") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: unsupported adaptor activation \"%s\" " - "(only \"relu\" is implemented)\n", + "(only \"relu\" is implemented)", hp.adaptor_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { - std::fprintf(stderr, "funasr_nano: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, - "funasr_nano: dec n_heads (%d) not divisible by n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: dec n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_activation != "silu" && hp.dec_activation != "swish") { - std::fprintf(stderr, - "funasr_nano: unsupported decoder activation \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: unsupported decoder activation \"%s\"", hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (!hp.dec_tie_word_embeddings) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: decoder.tie_word_embeddings=false is not supported " - "(graph reuses dec.token_embd.weight as the lm_head)\n"); + "(graph reuses dec.token_embd.weight as the lm_head)"); return TRANSCRIBE_ERR_GGUF; } if (hp.adaptor_llm_dim != hp.dec_hidden) { - std::fprintf(stderr, - "funasr_nano: adaptor.llm_dim (%d) != decoder.hidden_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: adaptor.llm_dim (%d) != decoder.hidden_size (%d)", hp.adaptor_llm_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "kaldi_fbank_lfr") { - std::fprintf(stderr, - "funasr_nano: unsupported frontend type \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: unsupported frontend type \"%s\"", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_window != "hamming") { - std::fprintf(stderr, - "funasr_nano: unsupported frontend window \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: unsupported frontend window \"%s\"", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_normalize != "none") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "funasr_nano: unsupported frontend normalize \"%s\" " - "(only \"none\" is implemented; CMVN was dropped at convert)\n", + "(only \"none\" is implemented; CMVN was dropped at convert)", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_fbank_style != "kaldi_htk") { - std::fprintf(stderr, - "funasr_nano: unsupported fbank_style \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: unsupported fbank_style \"%s\"", hp.fe_fbank_style.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -358,20 +359,20 @@ transcribe_status build_funasr_nano_weights(ggml_context * ctx_meta, { ggml_tensor * tw = ggml_get_tensor(ctx_meta, "dec.token_embd.weight"); if (tw == nullptr) { - std::fprintf(stderr, - "funasr_nano: missing tensor \"dec.token_embd.weight\"\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: missing tensor \"dec.token_embd.weight\""); return TRANSCRIBE_ERR_GGUF; } if (tw->ne[0] != hp.dec_hidden) { - std::fprintf(stderr, - "funasr_nano: dec.token_embd.weight ne[0]=%lld, expected %lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: dec.token_embd.weight ne[0]=%lld, expected %lld", static_cast(tw->ne[0]), static_cast(hp.dec_hidden)); return TRANSCRIBE_ERR_GGUF; } if (tw->ne[1] != hp.dec_vocab_size) { - std::fprintf(stderr, - "funasr_nano: dec.token_embd.weight ne[1]=%lld, expected %lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: dec.token_embd.weight ne[1]=%lld, expected %lld", static_cast(tw->ne[1]), static_cast(hp.dec_vocab_size)); return TRANSCRIBE_ERR_GGUF; @@ -404,8 +405,8 @@ transcribe_status build_funasr_nano_weights(ggml_context * ctx_meta, GET_LIN(b.ffn_down_w, lname("dec.layers.%d.ffn.down.weight", i), dec_im, dec_h); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, - "funasr_nano: ffn gate/up dtype mismatch at layer %d\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "funasr_nano: ffn gate/up dtype mismatch at layer %d", i); return TRANSCRIBE_ERR_GGUF; } } diff --git a/src/arch/gigaam/decoder.cpp b/src/arch/gigaam/decoder.cpp index 763be82e..53527354 100644 --- a/src/arch/gigaam/decoder.cpp +++ b/src/arch/gigaam/decoder.cpp @@ -16,6 +16,7 @@ #include "gigaam.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "weights.h" #include "ggml.h" @@ -64,8 +65,8 @@ void readback_f32(const ggml_tensor * t, std::vector & out) { const auto * tt = ggml_get_type_traits(t->type); if (tt == nullptr || tt->to_float == nullptr) { - std::fprintf(stderr, - "gigaam decoder: tensor \"%s\" type %s has no to_float\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam decoder: tensor \"%s\" type %s has no to_float", t->name, ggml_type_name(t->type)); out.clear(); return; @@ -369,9 +370,9 @@ transcribe_status decode_ctc_greedy(const HostDecoderWeights & host, if (static_cast(host.ctc_w.size()) != n_classes * d_model || static_cast(host.ctc_b.size()) != n_classes) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam decoder (ctc): host head shape mismatch " - "(ctc_w=%zu expected=%d, ctc_b=%zu expected=%d)\n", + "(ctc_w=%zu expected=%d, ctc_b=%zu expected=%d)", host.ctc_w.size(), n_classes * d_model, host.ctc_b.size(), n_classes); return TRANSCRIBE_ERR_INVALID_ARG; diff --git a/src/arch/gigaam/encoder.cpp b/src/arch/gigaam/encoder.cpp index 5ef9f19c..7a8d6b0f 100644 --- a/src/arch/gigaam/encoder.cpp +++ b/src/arch/gigaam/encoder.cpp @@ -21,6 +21,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -322,9 +323,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, { EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam encoder: invalid arg " - "(ctx=%p, n_mel_frames=%d)\n", + "(ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } @@ -336,8 +337,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.mel_in = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, n_mel_frames, hp.fe_num_mels, n_batch); if (eb.mel_in == nullptr) { - std::fprintf(stderr, - "gigaam encoder: failed to allocate mel_in tensor\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam encoder: failed to allocate mel_in tensor"); return eb; } ggml_set_name(eb.mel_in, "mel.in"); diff --git a/src/arch/gigaam/model.cpp b/src/arch/gigaam/model.cpp index 09a6e4c7..b31716ab 100644 --- a/src/arch/gigaam/model.cpp +++ b/src/arch/gigaam/model.cpp @@ -169,8 +169,8 @@ transcribe_status load(Loader & loader, ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "gigaam: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -239,8 +239,8 @@ transcribe_status load_ref_mel(const std::string & dir, std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { - std::fprintf(stderr, - "gigaam: cannot open mel ref '%s'\n", path.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: cannot open mel ref '%s'", path.c_str()); return TRANSCRIBE_ERR_FILE_NOT_FOUND; } const std::streamsize total_bytes = f.tellg(); @@ -248,9 +248,9 @@ transcribe_status load_ref_mel(const std::string & dir, if (total_bytes <= 0 || (static_cast(total_bytes) % (sizeof(float) * n_mels)) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam: mel ref '%s' size %lld not divisible by " - "n_mels=%d * 4\n", + "n_mels=%d * 4", path.c_str(), static_cast(total_bytes), n_mels); return TRANSCRIBE_ERR_GGUF; } @@ -261,7 +261,7 @@ transcribe_status load_ref_mel(const std::string & dir, f.read(reinterpret_cast(out.data()), static_cast(total_bytes)); if (static_cast(f.gcount()) != total_bytes) { - std::fprintf(stderr, "gigaam: short read on mel ref\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam: short read on mel ref"); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_OK; @@ -490,7 +490,7 @@ transcribe_status run(transcribe_session * session, // ----- Compute ------------------------------------------------------- const int64_t t_enc_start = ggml_time_us(); if (ggml_backend_sched_graph_compute(gc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "gigaam: graph_compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam: graph_compute failed"); return TRANSCRIBE_ERR_GGUF; } gc->t_encode_us = ggml_time_us() - t_enc_start; @@ -536,7 +536,7 @@ transcribe_status run(transcribe_session * session, { ggml_tensor * enc_t = eb.dumps.rnnt_encoded; if (enc_t == nullptr) { - std::fprintf(stderr, "gigaam: rnnt.encoded missing\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam: rnnt.encoded missing"); return TRANSCRIBE_ERR_GGUF; } const int T_enc = static_cast(enc_t->ne[1]); @@ -719,7 +719,7 @@ transcribe_status run_batch_encode(GigaamSession * gc, const int64_t t_enc_start = ggml_time_us(); if (ggml_backend_sched_graph_compute(gc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "gigaam run_batch: graph_compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam run_batch: graph_compute failed"); return TRANSCRIBE_ERR_GGUF; } gc->t_encode_us = ggml_time_us() - t_enc_start; diff --git a/src/arch/gigaam/weights.cpp b/src/arch/gigaam/weights.cpp index 32c9157c..dc057741 100644 --- a/src/arch/gigaam/weights.cpp +++ b/src/arch/gigaam/weights.cpp @@ -8,6 +8,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -39,8 +40,8 @@ transcribe_status read_gigaam_hparams(const gguf_context * gguf, } else if (head_kind_str == "ctc") { hp.head_kind = HeadKind::CTC; } else { - std::fprintf(stderr, - "gigaam: unsupported stt.gigaam.head_kind=%s\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: unsupported stt.gigaam.head_kind=%s", head_kind_str.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -60,21 +61,21 @@ transcribe_status read_gigaam_hparams(const gguf_context * gguf, if (auto st = read_required_string_kv(gguf, "stt.gigaam.encoder.conv_norm_type", kFamilyTag, hp.enc_conv_norm_type); st != TRANSCRIBE_OK) return st; if (hp.enc_n_heads <= 0 || hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "gigaam: invariant d_model %% n_heads != 0 " - "(d_model=%d, n_heads=%d)\n", + "(d_model=%d, n_heads=%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_self_attention_model != "rotary") { - std::fprintf(stderr, - "gigaam: unsupported self_attention_model=%s (expected rotary)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: unsupported self_attention_model=%s (expected rotary)", hp.enc_self_attention_model.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_conv_norm_type != "layer_norm") { - std::fprintf(stderr, - "gigaam: unsupported conv_norm_type=%s (expected layer_norm)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: unsupported conv_norm_type=%s (expected layer_norm)", hp.enc_conv_norm_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -87,8 +88,8 @@ transcribe_status read_gigaam_hparams(const gguf_context * gguf, if (auto st = read_required_u32_kv(gguf, "stt.gigaam.joint.num_classes", kFamilyTag, hp.joint_n_classes); st != TRANSCRIBE_OK) return st; if (auto st = read_required_string_kv(gguf, "stt.gigaam.joint.activation", kFamilyTag, hp.joint_activation); st != TRANSCRIBE_OK) return st; if (hp.pred_vocab != hp.joint_n_classes) { - std::fprintf(stderr, - "gigaam: pred_vocab (%d) != joint_n_classes (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: pred_vocab (%d) != joint_n_classes (%d)", hp.pred_vocab, hp.joint_n_classes); return TRANSCRIBE_ERR_GGUF; } @@ -96,8 +97,8 @@ transcribe_status read_gigaam_hparams(const gguf_context * gguf, if (auto st = read_required_u32_kv(gguf, "stt.gigaam.head.feat_in", kFamilyTag, hp.head_feat_in); st != TRANSCRIBE_OK) return st; if (auto st = read_required_u32_kv(gguf, "stt.gigaam.head.num_classes", kFamilyTag, hp.head_n_classes); st != TRANSCRIBE_OK) return st; if (hp.head_feat_in != hp.enc_d_model) { - std::fprintf(stderr, - "gigaam: ctc head.feat_in (%d) != encoder.d_model (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "gigaam: ctc head.feat_in (%d) != encoder.d_model (%d)", hp.head_feat_in, hp.enc_d_model); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/granite/decoder.cpp b/src/arch/granite/decoder.cpp index e80fc511..aae771df 100644 --- a/src/arch/granite/decoder.cpp +++ b/src/arch/granite/decoder.cpp @@ -19,6 +19,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -372,7 +373,7 @@ ggml_tensor * block_prefill_batched( const size_t v_elem = ggml_element_size(kv_cache.self_v); if (!use_flash) { - std::fprintf(stderr, "granite block_prefill_batched: requires flash\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite block_prefill_batched: requires flash"); return nullptr; } @@ -462,7 +463,7 @@ ggml_tensor * block_step_batched( const size_t v_elem = ggml_element_size(kv_cache.self_v); if (!use_flash) { - std::fprintf(stderr, "granite block_step_batched: requires flash\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite block_step_batched: requires flash"); return nullptr; } @@ -544,26 +545,26 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, pb.suffix_len = suffix_len; if (ctx == nullptr || T_prompt <= 0 || n_audio_tokens <= 0) { - std::fprintf(stderr, - "granite decoder: invalid arg (T_prompt=%d, n_audio=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite decoder: invalid arg (T_prompt=%d, n_audio=%d)", T_prompt, n_audio_tokens); return pb; } if (prefix_len < 0 || suffix_len < 0 || prefix_len + n_audio_tokens + suffix_len != T_prompt) { - std::fprintf(stderr, - "granite decoder: prefix(%d)+n_audio(%d)+suffix(%d) != T_prompt(%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite decoder: prefix(%d)+n_audio(%d)+suffix(%d) != T_prompt(%d)", prefix_len, n_audio_tokens, suffix_len, T_prompt); return pb; } if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { - std::fprintf(stderr, "granite decoder: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite decoder: kv_cache not initialized"); return pb; } if (T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, - "granite decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d", T_prompt, kv_cache.n_ctx); return pb; } @@ -598,8 +599,8 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "granite decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite decoder: ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -734,17 +735,17 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0) { - std::fprintf(stderr, - "granite step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, "granite step: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite step: kv_cache not initialized"); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "granite step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -776,8 +777,8 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "granite step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite step: ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -845,7 +846,7 @@ PrefillBuildBatched build_prefill_graph_batched( n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt_max > kv_cache.n_ctx) { - std::fprintf(stderr, "granite prefill(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite prefill(batched): invalid arg"); return pb; } @@ -932,7 +933,7 @@ StepBuildBatched build_step_graph_batched( if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "granite step(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite step(batched): invalid arg"); return sb; } diff --git a/src/arch/granite/encoder.cpp b/src/arch/granite/encoder.cpp index fc73779c..6f47a5d2 100644 --- a/src/arch/granite/encoder.cpp +++ b/src/arch/granite/encoder.cpp @@ -35,6 +35,7 @@ #include "granite_conformer/shaw_attn.h" #include "transcribe-debug.h" #include "transcribe-mel.h" +#include "transcribe-log.h" #include "ggml.h" @@ -134,8 +135,8 @@ transcribe_status compute_mel_encoder_input( // recover that as raw.size() / n_mels. const int stride = static_cast(raw.size() / static_cast(n_mels)); if (stride <= 0 || stride < n_frames) { - std::fprintf(stderr, - "granite: mel buffer stride (%d) < expected frames (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: mel buffer stride (%d) < expected frames (%d)", stride, n_frames); return TRANSCRIBE_ERR_GGUF; } @@ -558,7 +559,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "granite encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/granite/model.cpp b/src/arch/granite/model.cpp index e0cf3f33..67d43479 100644 --- a/src/arch/granite/model.cpp +++ b/src/arch/granite/model.cpp @@ -255,8 +255,8 @@ transcribe_status resolve_chat_tokens(const transcribe::Tokenizer & tok, for (const auto & p : required) { const int id = tok.find(p.piece); if (id < 0) { - std::fprintf(stderr, - "granite: tokenizer missing required piece \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: tokenizer missing required piece \"%s\"", p.piece); return TRANSCRIBE_ERR_GGUF; } @@ -350,13 +350,13 @@ transcribe_status load( m->hparams.pad_token_id = m->chat_tokens.pad; if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "granite: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, "granite: tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } @@ -444,8 +444,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "granite: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -565,15 +565,15 @@ static transcribe_status build_granite_affixes( if (params->task == TRANSCRIBE_TASK_TRANSLATE) { if (params->target_language == nullptr || params->target_language[0] == '\0') { - std::fprintf(stderr, - "granite: translate task requires --target-language\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: translate task requires --target-language"); return TRANSCRIBE_ERR_INVALID_ARG; } const char * lang_name = granite_target_language_name(params->target_language); if (lang_name == nullptr) { - std::fprintf(stderr, - "granite: target_language '%s' is not advertised\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: target_language '%s' is not advertised", params->target_language); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -636,12 +636,12 @@ static transcribe_status build_granite_affixes( const std::string suffix_text = instruction + "\n ASSISTANT:"; if (const transcribe_status st = cm->tok.encode(prefix_text, prefix_ids); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "granite: tokenize(prefix) failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: tokenize(prefix) failed"); return st; } if (const transcribe_status st = cm->tok.encode(suffix_text, suffix_ids); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "granite: tokenize(suffix) failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: tokenize(suffix) failed"); return st; } } @@ -663,7 +663,7 @@ transcribe_status run( transcribe::debug::init(); if (!cm->mel.has_value()) { - std::fprintf(stderr, "granite run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -674,8 +674,8 @@ transcribe_status run( *cm->mel, pcm, n_samples, cc->n_threads, cc->mel_buf, t_enc); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "granite run: mel/stack failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: mel/stack failed (%s)", transcribe_status_string(mst)); return mst; } @@ -731,8 +731,8 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 32768, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "granite run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -807,8 +807,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite run: encoder graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: encoder graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -899,8 +899,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite run: projector graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: projector graph compute failed (%d)", static_cast(gs)); ggml_free(proj_ctx); return TRANSCRIBE_ERR_GGUF; @@ -1102,8 +1102,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, dec.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite run: decoder prefill compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: decoder prefill compute failed (%d)", static_cast(gs)); ggml_free(dec_ctx); return TRANSCRIBE_ERR_GGUF; @@ -1216,8 +1216,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, step.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite run: step compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite run: step compute failed (%d)", static_cast(gs)); ggml_free(step_ctx); return TRANSCRIBE_ERR_GGUF; @@ -1718,9 +1718,9 @@ transcribe_status run_batch( int total_steps = 0; for (int b = 0; b < n; ++b) total_steps = std::max(total_steps, static_cast(generated[b].size())); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "granite run_batch: n=%d max_n_kv=%d steps=%d max_T_prompt=%d n_audio_max=%d\n" - " mel=%.1fms enc+proj=%.1fms (serial x%d) prefill=%.1fms (batched) step_loop=%.1fms (%.2fms/step, batched)\n", + " mel=%.1fms enc+proj=%.1fms (serial x%d) prefill=%.1fms (batched) step_loop=%.1fms (%.2fms/step, batched)", n, max_n_kv, total_steps, max_T_prompt, n_audio_max, mel_us / 1000.0, enc_us / 1000.0, n, prefill_us / 1000.0, step_us / 1000.0, diff --git a/src/arch/granite/projector.cpp b/src/arch/granite/projector.cpp index 8a2aaeee..cac2a1e2 100644 --- a/src/arch/granite/projector.cpp +++ b/src/arch/granite/projector.cpp @@ -53,6 +53,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -219,9 +220,9 @@ ProjectorBuild build_projector_graph(ggml_context * ctx, const float ln_eps = hp.prj_layer_norm_eps; // 1e-12 if (prj_hidden % n_heads != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite projector: prj_hidden (%d) not divisible by " - "n_heads (%d)\n", prj_hidden, n_heads); + "n_heads (%d)", prj_hidden, n_heads); return pb; } const int head_dim = prj_hidden / n_heads; // 64 @@ -334,7 +335,7 @@ ProjectorBuild build_projector_graph(ggml_context * ctx, // 4096 nodes is plenty of headroom. pb.graph = ggml_new_graph_custom(ctx, /*size=*/4096, /*grads=*/false); if (pb.graph == nullptr) { - std::fprintf(stderr, "granite projector: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite projector: ggml_new_graph_custom failed"); return pb; } ggml_build_forward_expand(pb.graph, pb.out); diff --git a/src/arch/granite/weights.cpp b/src/arch/granite/weights.cpp index 75df1621..1b8c0521 100644 --- a/src/arch/granite/weights.cpp +++ b/src/arch/granite/weights.cpp @@ -9,6 +9,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -56,8 +57,8 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, case KvResult::Ok: hp.enc_cat_hidden_layers = std::move(tmp); break; case KvResult::Absent: hp.enc_cat_hidden_layers.clear(); break; case KvResult::BadType: - std::fprintf(stderr, - "granite: stt.granite.encoder.cat_hidden_layers has wrong type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: stt.granite.encoder.cat_hidden_layers has wrong type"); return TRANSCRIBE_ERR_GGUF; } } @@ -124,19 +125,19 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, hp.enc_conv_expansion <= 0 || hp.enc_max_pos_emb <= 0 || hp.enc_context_size <= 0) { - std::fprintf(stderr, "granite: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_n_heads * hp.enc_head_dim != hp.enc_hidden) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: encoder n_heads * head_dim (%d * %d) " - "!= hidden (%d)\n", + "!= hidden (%d)", hp.enc_n_heads, hp.enc_head_dim, hp.enc_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_context_size > hp.enc_max_pos_emb) { - std::fprintf(stderr, - "granite: context_size (%d) > max_pos_emb (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: context_size (%d) > max_pos_emb (%d)", hp.enc_context_size, hp.enc_max_pos_emb); return TRANSCRIBE_ERR_GGUF; } @@ -146,9 +147,9 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, const int32_t expected_kv_dim = hp.enc_hidden * (static_cast(hp.enc_cat_hidden_layers.size()) + 1); if (expected_kv_dim != hp.prj_encoder_hidden_size) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: prj.encoder_hidden_size (%d) does not match " - "hidden*(1+|cat_hidden_layers|) = %d * (1 + %d) = %d\n", + "hidden*(1+|cat_hidden_layers|) = %d * (1 + %d) = %d", hp.prj_encoder_hidden_size, hp.enc_hidden, static_cast(hp.enc_cat_hidden_layers.size()), expected_kv_dim); @@ -156,9 +157,9 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, } for (int32_t idx : hp.enc_cat_hidden_layers) { if (idx < 0 || idx >= hp.enc_n_layers) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: cat_hidden_layers entry %d out of range " - "[0, %d)\n", idx, hp.enc_n_layers); + "[0, %d)", idx, hp.enc_n_layers); return TRANSCRIBE_ERR_GGUF; } } @@ -167,49 +168,49 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, hp.prj_intermediate <= 0 || hp.prj_n_heads <= 0 || hp.prj_encoder_hidden_size <= 0) { - std::fprintf(stderr, "granite: projector hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: projector hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.prj_hidden % hp.prj_n_heads != 0) { - std::fprintf(stderr, - "granite: projector hidden (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: projector hidden (%d) not divisible by n_heads (%d)", hp.prj_hidden, hp.prj_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.prj_hidden_act != "gelu") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported projector hidden_act \"%s\" " - "(only gelu)\n", hp.prj_hidden_act.c_str()); + "(only gelu)", hp.prj_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.prj_pos_embed_type != "absolute") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported projector position_embedding_type " - "\"%s\" (only absolute)\n", hp.prj_pos_embed_type.c_str()); + "\"%s\" (only absolute)", hp.prj_pos_embed_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { - std::fprintf(stderr, "granite: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, - "granite: dec n_heads (%d) not divisible by n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: dec n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads * hp.dec_head_dim != hp.dec_hidden) { - std::fprintf(stderr, - "granite: dec n_heads * head_dim (%d * %d) != hidden (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: dec n_heads * head_dim (%d * %d) != hidden (%d)", hp.dec_n_heads, hp.dec_head_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden_act != "silu" && hp.dec_hidden_act != "swish") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported decoder hidden_act \"%s\" " - "(only silu/swish)\n", hp.dec_hidden_act.c_str()); + "(only silu/swish)", hp.dec_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_embedding_multiplier <= 0.0f || @@ -217,15 +218,15 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, hp.dec_attention_multiplier <= 0.0f || hp.dec_residual_multiplier <= 0.0f) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: decoder multipliers must be > 0 " - "(emb=%g, logits=%g, attn=%g, residual=%g)\n", + "(emb=%g, logits=%g, attn=%g, residual=%g)", hp.dec_embedding_multiplier, hp.dec_logits_scaling, hp.dec_attention_multiplier, hp.dec_residual_multiplier); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, "granite: unsupported frontend type \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported frontend type \"%s\"", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -234,15 +235,15 @@ transcribe_status read_granite_hparams(const gguf_context * gguf, // normalization (log10 → max-8 floor → /4 + 1). The C++ MelFrontend // produces a bit-identical result under "per_utterance" mode. // Other modes would mean the converter changed; fail loudly. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported frontend normalize \"%s\" " - "(only \"per_utterance\")\n", hp.fe_normalize.c_str()); + "(only \"per_utterance\")", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_mel_norm != "htk") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite: unsupported frontend mel_norm \"%s\" " - "(only \"htk\")\n", hp.fe_mel_norm.c_str()); + "(only \"htk\")", hp.fe_mel_norm.c_str()); return TRANSCRIBE_ERR_GGUF; } // Audio injection precondition: projector lifts to LM hidden, so @@ -478,8 +479,8 @@ transcribe_status build_granite_weights(ggml_context * ctx_meta, GET_LIN(b.ffn_down_w, lname("dec.blocks.%d.ffn.down.weight", i), dec_im, dec_h); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, - "granite: ffn gate/up dtype mismatch at dec layer %d\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite: ffn gate/up dtype mismatch at dec layer %d", i); return TRANSCRIBE_ERR_GGUF; } } diff --git a/src/arch/granite_nar/decoder.cpp b/src/arch/granite_nar/decoder.cpp index 734c48f0..0b1d7a9b 100644 --- a/src/arch/granite_nar/decoder.cpp +++ b/src/arch/granite_nar/decoder.cpp @@ -9,6 +9,7 @@ #include "granite_nar.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -164,8 +165,8 @@ ForwardBuild build_forward_graph(ggml_context * ctx, fb.T_total = n_audio_tokens + n_text; if (ctx == nullptr || n_audio_tokens <= 0 || n_text <= 0) { - std::fprintf(stderr, - "granite_nar decoder: invalid (n_audio=%d, n_text=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar decoder: invalid (n_audio=%d, n_text=%d)", n_audio_tokens, n_text); return fb; } @@ -191,7 +192,7 @@ ForwardBuild build_forward_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "granite_nar decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar decoder: ggml_new_graph_custom failed"); return fb; } fb.graph = gf; diff --git a/src/arch/granite_nar/encoder.cpp b/src/arch/granite_nar/encoder.cpp index 5d471c78..c93d9a62 100644 --- a/src/arch/granite_nar/encoder.cpp +++ b/src/arch/granite_nar/encoder.cpp @@ -23,6 +23,7 @@ #include "granite_conformer/shaw_attn.h" #include "transcribe-debug.h" #include "transcribe-mel.h" +#include "transcribe-log.h" #include "ggml.h" @@ -101,8 +102,8 @@ transcribe_status compute_mel_encoder_input( const int stride = static_cast(raw.size() / static_cast(n_mels)); if (stride <= 0 || stride < n_frames) { - std::fprintf(stderr, - "granite_nar: mel buffer stride (%d) < expected frames (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar: mel buffer stride (%d) < expected frames (%d)", stride, n_frames); return TRANSCRIBE_ERR_GGUF; } @@ -437,8 +438,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, ggml_tensor * cat = nullptr; for (size_t k = 0; k < captures.size(); ++k) { if (captures[k] == nullptr) { - std::fprintf(stderr, - "granite_nar encoder: missing capture for layer index %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar encoder: missing capture for layer index %d", (int)hp.enc_layer_indices[k]); return eb; } @@ -451,7 +452,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "granite_nar encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.cat_out); @@ -489,8 +490,8 @@ void compute_bpe_ctc_initial_hypothesis( return; } if (static_cast(importance_non_blank.size()) < T_enc) { - std::fprintf(stderr, - "granite_nar BPE CTC: importance has %zu entries, need >= %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar BPE CTC: importance has %zu entries, need >= %d", importance_non_blank.size(), T_enc); return; } diff --git a/src/arch/granite_nar/model.cpp b/src/arch/granite_nar/model.cpp index ba9c1c4e..500eab57 100644 --- a/src/arch/granite_nar/model.cpp +++ b/src/arch/granite_nar/model.cpp @@ -254,12 +254,12 @@ transcribe_status load( m->hparams.dec_eos_id = m->tok.eos_id(); if (m->hparams.dec_eos_id < 0) { - std::fprintf(stderr, "granite_nar: tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar: tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } if (m->tok.n_tokens() != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "granite_nar: tokenizer vocab (%d) != dec_vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar: tokenizer vocab (%d) != dec_vocab_size (%d)", m->tok.n_tokens(), m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } @@ -378,7 +378,7 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, "granite_nar: alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar: alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -461,7 +461,7 @@ transcribe_status run( transcribe::debug::init(); if (!cm->mel.has_value()) { - std::fprintf(stderr, "granite_nar run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -472,8 +472,8 @@ transcribe_status run( *cm->mel, pcm, n_samples, cc->n_threads, cc->mel_buf, t_enc); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "granite_nar run: mel/stack failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar run: mel/stack failed (%s)", transcribe_status_string(mst)); return mst; } @@ -521,7 +521,7 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 32768, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, "granite_nar run: sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_nar run: sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -569,8 +569,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite_nar run: encoder compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar run: encoder compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -703,8 +703,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite_nar run: projector compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar run: projector compute failed (%d)", static_cast(gs)); ggml_free(proj_ctx); return TRANSCRIBE_ERR_GGUF; @@ -812,8 +812,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, fb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "granite_nar run: decoder compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar run: decoder compute failed (%d)", static_cast(gs)); ggml_free(dec_ctx); return TRANSCRIBE_ERR_GGUF; diff --git a/src/arch/granite_nar/projector.cpp b/src/arch/granite_nar/projector.cpp index 29dbd887..d2186de1 100644 --- a/src/arch/granite_nar/projector.cpp +++ b/src/arch/granite_nar/projector.cpp @@ -11,6 +11,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -102,8 +103,8 @@ ProjectorBuild build_projector_graph(ggml_context * ctx, const float ln_eps = hp.prj_layernorm_eps; if (prj_hidden % n_heads != 0) { - std::fprintf(stderr, - "granite_nar projector: prj_hidden (%d) %% n_heads (%d) != 0\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar projector: prj_hidden (%d) %% n_heads (%d) != 0", prj_hidden, n_heads); return pb; } @@ -245,8 +246,8 @@ ProjectorBuild build_projector_graph(ggml_context * ctx, pb.graph = ggml_new_graph_custom(ctx, /*size=*/4096, /*grads=*/false); if (pb.graph == nullptr) { - std::fprintf(stderr, - "granite_nar projector: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "granite_nar projector: ggml_new_graph_custom failed"); return pb; } ggml_build_forward_expand(pb.graph, pb.out); diff --git a/src/arch/granite_nar/weights.cpp b/src/arch/granite_nar/weights.cpp index add04c1b..e9b9ac75 100644 --- a/src/arch/granite_nar/weights.cpp +++ b/src/arch/granite_nar/weights.cpp @@ -4,6 +4,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -83,7 +84,7 @@ transcribe_status read_granite_nar_hparams(const gguf_context * gguf, case KvResult::Ok: hp.enc_layer_indices = std::move(tmp); break; case KvResult::Absent: hp.enc_layer_indices = {-1}; break; case KvResult::BadType: - std::fprintf(stderr, "%s: encoder.layer_indices wrong type\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: encoder.layer_indices wrong type", kTag); return TRANSCRIBE_ERR_GGUF; } } @@ -149,20 +150,20 @@ transcribe_status read_granite_nar_hparams(const gguf_context * gguf, case KvResult::Ok: hp.ctc_chars = std::move(tmp); break; case KvResult::Absent: hp.ctc_chars.clear(); break; case KvResult::BadType: - std::fprintf(stderr, "%s: ctc_chars wrong type\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: ctc_chars wrong type", kTag); return TRANSCRIBE_ERR_GGUF; } } // Cross-field invariants. if (hp.prj_num_encoder_layers != (int32_t)hp.enc_layer_indices.size()) { - std::fprintf(stderr, "%s: prj_num_encoder_layers (%d) != " - "len(enc_layer_indices) (%zu)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: prj_num_encoder_layers (%d) != " + "len(enc_layer_indices) (%zu)", kTag, hp.prj_num_encoder_layers, hp.enc_layer_indices.size()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden != hp.prj_llm_dim) { - std::fprintf(stderr, "%s: dec_hidden (%d) != prj_llm_dim (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: dec_hidden (%d) != prj_llm_dim (%d)", kTag, hp.dec_hidden, hp.prj_llm_dim); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/medasr/encoder.cpp b/src/arch/medasr/encoder.cpp index 7c0e0618..d9ecffef 100644 --- a/src/arch/medasr/encoder.cpp +++ b/src/arch/medasr/encoder.cpp @@ -41,6 +41,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -464,9 +465,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, { EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr encoder: invalid arg " - "(ctx=%p, n_mel_frames=%d)\n", + "(ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } @@ -632,8 +633,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // CTC head ~ 1000 nodes; 8192 leaves ample headroom. eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "medasr encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "medasr encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/medasr/model.cpp b/src/arch/medasr/model.cpp index 69a10051..47fb1df3 100644 --- a/src/arch/medasr/model.cpp +++ b/src/arch/medasr/model.cpp @@ -204,8 +204,8 @@ transcribe_status load(Loader & loader, ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "medasr: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "medasr: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -323,9 +323,9 @@ transcribe_status load_ref_mel(const std::string & dir, used = base + "frontend.mel.out.f32"; } if (!f) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: cannot open mel ref under '%s' " - "(expected mel.in.f32 or frontend.mel.out.f32)\n", + "(expected mel.in.f32 or frontend.mel.out.f32)", dir.c_str()); return TRANSCRIBE_ERR_FILE_NOT_FOUND; } @@ -335,9 +335,9 @@ transcribe_status load_ref_mel(const std::string & dir, if (total_bytes <= 0 || (static_cast(total_bytes) % (sizeof(float) * n_mels)) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: mel ref '%s' size %lld not divisible by " - "n_mels=%d * 4\n", + "n_mels=%d * 4", used.c_str(), static_cast(total_bytes), n_mels); return TRANSCRIBE_ERR_GGUF; @@ -348,7 +348,7 @@ transcribe_status load_ref_mel(const std::string & dir, f.read(reinterpret_cast(out.data()), static_cast(total_bytes)); if (static_cast(f.gcount()) != total_bytes) { - std::fprintf(stderr, "medasr: short read on mel ref\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: short read on mel ref"); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_OK; @@ -564,7 +564,7 @@ transcribe_status run(transcribe_session * session, const int64_t t_enc_start = ggml_time_us(); if (ggml_backend_sched_graph_compute(gc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "medasr: graph_compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: graph_compute failed"); return TRANSCRIBE_ERR_GGUF; } gc->t_encode_us = ggml_time_us() - t_enc_start; @@ -613,8 +613,8 @@ transcribe_status run(transcribe_session * session, if (v > mx) mx = v; } const double mean = n > 0 ? sum / static_cast(n) : 0.0; - std::fprintf(stderr, - "STATS %-26s n=%zu min=%+.4e max=%+.4e mean=%+.4e nan=%d inf=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "STATS %-26s n=%zu min=%+.4e max=%+.4e mean=%+.4e nan=%d inf=%d", name, n, mn, mx, mean, n_nan, n_inf); }; report("mel.in", eb.mel_in); @@ -661,8 +661,8 @@ transcribe_status run(transcribe_session * session, const int vocab = static_cast(logits_t->ne[0]); const int T_enc = static_cast(logits_t->ne[1]); if (vocab != gm->hparams.ctc_vocab_size || T_enc <= 0) { - std::fprintf(stderr, - "medasr: ctc_logits shape mismatch (vocab=%d T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "medasr: ctc_logits shape mismatch (vocab=%d T_enc=%d)", vocab, T_enc); return TRANSCRIBE_ERR_GGUF; } @@ -957,7 +957,7 @@ transcribe_status run_batch_encode(MedAsrSession * gc, const int64_t t_enc_start = ggml_time_us(); if (ggml_backend_sched_graph_compute(gc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "medasr run_batch: graph_compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr run_batch: graph_compute failed"); return TRANSCRIBE_ERR_GGUF; } gc->t_encode_us = ggml_time_us() - t_enc_start; diff --git a/src/arch/medasr/weights.cpp b/src/arch/medasr/weights.cpp index bf7e7fdf..99a0b785 100644 --- a/src/arch/medasr/weights.cpp +++ b/src/arch/medasr/weights.cpp @@ -11,6 +11,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-backend.h" @@ -58,22 +59,22 @@ transcribe_status read_medasr_hparams(const gguf_context * gguf, if (auto st = read_required_u32_kv (gguf, "stt.medasr.encoder.sub_n_layers", kFamilyTag, hp.enc_sub_n_layers); st != TRANSCRIBE_OK) return st; if (hp.enc_n_heads <= 0 || hp.enc_hidden % hp.enc_n_heads != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: invariant hidden %% n_heads != 0 " - "(hidden=%d, n_heads=%d)\n", + "(hidden=%d, n_heads=%d)", hp.enc_hidden, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_sub_n_layers != 2 || hp.enc_sub_stride != 2) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr: unsupported subsampling shape (n_layers=%d, " - "stride=%d) — only 2 stride-2 convs implemented\n", + "stride=%d) — only 2 stride-2 convs implemented", hp.enc_sub_n_layers, hp.enc_sub_stride); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_rope_type != "default") { - std::fprintf(stderr, - "medasr: unsupported rope_type=%s (expected default)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "medasr: unsupported rope_type=%s (expected default)", hp.enc_rope_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -276,9 +277,9 @@ transcribe_status fuse_batch_norm(const gguf_context * /*gguf_data*/, return TRANSCRIBE_ERR_INVALID_ARG; } if (static_cast(g_raw_bn.size()) != hp.enc_n_layers) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr fuse_batch_norm: raw BN catalog size %zu != " - "n_layers %d\n", + "n_layers %d", g_raw_bn.size(), hp.enc_n_layers); return TRANSCRIBE_ERR_GGUF; } @@ -303,9 +304,9 @@ transcribe_status fuse_batch_norm(const gguf_context * /*gguf_data*/, const RawBnTensors & raw = g_raw_bn[static_cast(i)]; if (raw.w == nullptr || raw.b == nullptr || raw.mean == nullptr || raw.var == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "medasr fuse_batch_norm: missing raw BN tensor at " - "layer %d\n", i); + "layer %d", i); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_get(raw.w, bn_w.data(), 0, d * sizeof(float)); diff --git a/src/arch/moonshine/decoder.cpp b/src/arch/moonshine/decoder.cpp index 05289a1d..df4e7f86 100644 --- a/src/arch/moonshine/decoder.cpp +++ b/src/arch/moonshine/decoder.cpp @@ -25,6 +25,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -479,8 +480,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || T_enc <= 0) { - std::fprintf(stderr, - "moonshine cross_kv: invalid arg (ctx=%p, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine cross_kv: invalid arg (ctx=%p, T_enc=%d)", static_cast(ctx), T_enc); return db; } @@ -495,8 +496,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 4096, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "moonshine cross_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine cross_kv: ggml_new_graph_custom failed"); return db; } @@ -547,16 +548,16 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || n_tokens <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine decoder_kv: invalid arg " - "(ctx=%p, n_tokens=%d, T_enc=%d)\n", + "(ctx=%p, n_tokens=%d, T_enc=%d)", static_cast(ctx), n_tokens, T_enc); return db; } const int n_kv = n_past + n_tokens; if (n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "moonshine decoder_kv: n_kv=%d exceeds n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine decoder_kv: n_kv=%d exceeds n_ctx=%d", n_kv, kv_cache.n_ctx); return db; } @@ -615,8 +616,8 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "moonshine decoder_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine decoder_kv: ggml_new_graph_custom failed"); return db; } @@ -737,14 +738,14 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "moonshine step: invalid arg (max_n_kv=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine step: invalid arg (max_n_kv=%d, T_enc=%d)", max_n_kv, T_enc); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "moonshine step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -770,7 +771,7 @@ StepBuild build_step_graph(ggml_context * ctx, sb.graph = ggml_new_graph_custom(ctx, 8192, false); if (sb.graph == nullptr) { - std::fprintf(stderr, "moonshine step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine step: ggml_new_graph_custom failed"); return sb; } @@ -998,7 +999,7 @@ StepBuildBatched build_step_graph_batched(ggml_context * ctx, sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || T_enc_max <= 0 || n_batch <= 0) return sb; if (!use_flash) { - std::fprintf(stderr, "moonshine step(batched): requires flash path\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine step(batched): requires flash path"); return sb; } diff --git a/src/arch/moonshine/encoder.cpp b/src/arch/moonshine/encoder.cpp index 19c5e0ea..7866c0b4 100644 --- a/src/arch/moonshine/encoder.cpp +++ b/src/arch/moonshine/encoder.cpp @@ -45,6 +45,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -274,8 +275,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_samples <= 0) { - std::fprintf(stderr, - "moonshine encoder: invalid arg (ctx=%p, n_samples=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine encoder: invalid arg (ctx=%p, n_samples=%d)", static_cast(ctx), n_samples); return eb; } @@ -285,9 +286,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int T_enc = encoder_t_enc(hp, n_samples); if (T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine encoder: input too short (n_samples=%d " - "produces T_enc<=0)\n", n_samples); + "produces T_enc<=0)", n_samples); return eb; } @@ -413,8 +414,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, 8192, false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "moonshine encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/moonshine/model.cpp b/src/arch/moonshine/model.cpp index 0ad6ff95..9b6414a2 100644 --- a/src/arch/moonshine/model.cpp +++ b/src/arch/moonshine/model.cpp @@ -120,9 +120,9 @@ bool kv_cache_init(MoonshineKvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine kv_cache: unsupported kv_type=%d " - "(only F16/F32)\n", static_cast(kv_type)); + "(only F16/F32)", static_cast(kv_type)); return false; } @@ -130,7 +130,7 @@ bool kv_cache_init(MoonshineKvCache & cache, ggml_init_params params { ctx_size, nullptr, /*no_alloc=*/true }; cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "moonshine kv_cache: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine kv_cache: ggml_init failed"); return false; } @@ -149,7 +149,7 @@ bool kv_cache_init(MoonshineKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "moonshine kv_cache: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine kv_cache: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -181,7 +181,7 @@ bool kv_cache_init_batched(MoonshineKvCache & cache, return true; } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, "moonshine kv_cache(batched): unsupported kv_type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine kv_cache(batched): unsupported kv_type"); return false; } const size_t ctx_size = 4 * ggml_tensor_overhead() + 256; @@ -281,8 +281,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "moonshine: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -428,7 +428,7 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, "moonshine run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -452,7 +452,7 @@ transcribe_status run( apply_thread_policy(cc); if (ggml_backend_sched_graph_compute(cc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "moonshine run: encoder compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine run: encoder compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -534,7 +534,7 @@ transcribe_status run( if (ggml_backend_sched_graph_compute(cc->sched, cross_db.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "moonshine run: cross_kv compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine run: cross_kv compute failed"); return TRANSCRIBE_ERR_GGUF; } cc->kv_cache.cross_populated = true; @@ -615,8 +615,8 @@ transcribe_status run( if (ggml_backend_sched_graph_compute(cc->sched, db.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine run: decoder compute failed (n_past=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine run: decoder compute failed (n_past=%d)", n_past_in); return TRANSCRIBE_ERR_GGUF; } @@ -722,8 +722,8 @@ transcribe_status run( cc->compute_ctx, cm->weights, hp, cc->kv_cache, max_n_kv, T_enc, cc->decoder_use_flash); if (sb.graph == nullptr || sb.argmax_out == nullptr) { - std::fprintf(stderr, - "moonshine run: build_step_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine run: build_step_graph failed"); return TRANSCRIBE_ERR_GGUF; } ggml_backend_sched_reset(cc->sched); @@ -744,8 +744,8 @@ transcribe_status run( while (next_token != eos && n_past < max_pos) { if (cc->poll_abort()) return TRANSCRIBE_ERR_ABORTED; if (n_past + 1 > max_n_kv) { - std::fprintf(stderr, - "moonshine run: hit max_n_kv=%d at n_past=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, + "moonshine run: hit max_n_kv=%d at n_past=%d", max_n_kv, n_past); break; } @@ -765,8 +765,8 @@ transcribe_status run( if (ggml_backend_sched_graph_compute(cc->sched, sb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine run: step compute failed (n_past=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine run: step compute failed (n_past=%d)", n_past); return TRANSCRIBE_ERR_GGUF; } @@ -1174,9 +1174,9 @@ transcribe_status run_batch( } if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "moonshine run_batch: n=%d T_enc_max=%d max_n_kv=%d\n" - " enc=%.1fms (serial x%d) decode=%.1fms (batched)\n", + " enc=%.1fms (serial x%d) decode=%.1fms (batched)", n, T_enc_max, max_n_kv, enc_us / 1000.0, n, dec_us / 1000.0); } return TRANSCRIBE_OK; diff --git a/src/arch/moonshine/weights.cpp b/src/arch/moonshine/weights.cpp index 5d827c19..6da3b09f 100644 --- a/src/arch/moonshine/weights.cpp +++ b/src/arch/moonshine/weights.cpp @@ -22,6 +22,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -53,7 +54,7 @@ transcribe_status read_optional_u32_kv(const gguf_context * gguf, out = default_value; return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -73,7 +74,7 @@ transcribe_status read_optional_f32_kv(const gguf_context * gguf, out = default_value; return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -86,17 +87,17 @@ transcribe_status read_required_u32_array_kv(const gguf_context * gguf, if (gguf == nullptr) return TRANSCRIBE_ERR_INVALID_ARG; const int64_t kid = gguf_find_key(gguf, key); if (kid < 0) { - std::fprintf(stderr, "%s: missing required KV %s\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: missing required KV %s", error_tag, key); return TRANSCRIBE_ERR_GGUF; } if (gguf_get_kv_type(gguf, kid) != GGUF_TYPE_ARRAY) { - std::fprintf(stderr, "%s: KV %s is not an array\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s is not an array", error_tag, key); return TRANSCRIBE_ERR_GGUF; } const gguf_type elem_type = gguf_get_arr_type(gguf, kid); if (elem_type != GGUF_TYPE_UINT32 && elem_type != GGUF_TYPE_INT32) { - std::fprintf(stderr, - "%s: KV %s array element type is not u32/i32 (got %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: KV %s array element type is not u32/i32 (got %d)", error_tag, key, static_cast(elem_type)); return TRANSCRIBE_ERR_GGUF; } @@ -177,11 +178,11 @@ transcribe_status read_moonshine_hparams(const gguf_context * gguf, // ----- Cross-field invariants ----- if (hp.enc_n_layers <= 0 || hp.enc_d_model <= 0 || hp.enc_n_heads <= 0 || hp.enc_ffn_dim <= 0) { - std::fprintf(stderr, "moonshine: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, "moonshine: enc d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: enc d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } @@ -189,69 +190,69 @@ transcribe_status read_moonshine_hparams(const gguf_context * gguf, hp.dec_ffn_dim <= 0 || hp.dec_max_position_embeddings <= 0 || hp.dec_vocab_size <= 0) { - std::fprintf(stderr, "moonshine: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_d_model % hp.dec_n_heads != 0) { - std::fprintf(stderr, "moonshine: dec d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: dec d_model (%d) not divisible by n_heads (%d)", hp.dec_d_model, hp.dec_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model != hp.dec_d_model) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: encoder d_model (%d) != decoder d_model (%d); " - "cross-attn would mismatch\n", + "cross-attn would mismatch", hp.enc_d_model, hp.dec_d_model); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_activation != "gelu") { - std::fprintf(stderr, - "moonshine: only \"gelu\" encoder activation is supported (got \"%s\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: only \"gelu\" encoder activation is supported (got \"%s\")", hp.enc_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_activation != "silu") { - std::fprintf(stderr, - "moonshine: only \"silu\" decoder activation is supported (got \"%s\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: only \"silu\" decoder activation is supported (got \"%s\")", hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (!hp.dec_tie_word_embeddings) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: stt.moonshine.decoder.tie_word_embeddings=false " - "is not supported (no separate lm_head tensor)\n"); + "is not supported (no separate lm_head tensor)"); return TRANSCRIBE_ERR_GGUF; } if (hp.attention_bias) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: stt.moonshine.attention_bias=true is not supported " - "(catalog has no attn bias slots)\n"); + "(catalog has no attn bias slots)"); return TRANSCRIBE_ERR_GGUF; } if (hp.partial_rotary_factor <= 0.0f || hp.partial_rotary_factor > 1.0f) { - std::fprintf(stderr, - "moonshine: invalid partial_rotary_factor=%g (expected (0, 1])\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: invalid partial_rotary_factor=%g (expected (0, 1])", hp.partial_rotary_factor); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "raw") { - std::fprintf(stderr, - "moonshine: unsupported frontend type \"%s\" (only \"raw\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: unsupported frontend type \"%s\" (only \"raw\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_sample_rate != 16000) { - std::fprintf(stderr, - "moonshine: unsupported sample_rate=%d (only 16000 Hz)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: unsupported sample_rate=%d (only 16000 Hz)", hp.fe_sample_rate); return TRANSCRIBE_ERR_GGUF; } if (hp.conv_channels.size() != 3 || hp.conv_kernel_sizes.size() != 3 || hp.conv_strides.size() != 3) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: conv stem expected 3 entries each " - "(channels=%zu, kernel_sizes=%zu, strides=%zu)\n", + "(channels=%zu, kernel_sizes=%zu, strides=%zu)", hp.conv_channels.size(), hp.conv_kernel_sizes.size(), hp.conv_strides.size()); @@ -260,22 +261,22 @@ transcribe_status read_moonshine_hparams(const gguf_context * gguf, if (hp.conv_channels[0] != hp.enc_d_model || hp.conv_channels[2] != hp.enc_d_model) { - std::fprintf(stderr, - "moonshine: conv stem channels [%d, %d, %d] disagree with enc_d_model=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: conv stem channels [%d, %d, %d] disagree with enc_d_model=%d", hp.conv_channels[0], hp.conv_channels[1], hp.conv_channels[2], hp.enc_d_model); return TRANSCRIBE_ERR_GGUF; } if (hp.conv_groupnorm_num_groups != 1) { - std::fprintf(stderr, - "moonshine: only num_groups=1 GroupNorm is supported (got %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine: only num_groups=1 GroupNorm is supported (got %d)", hp.conv_groupnorm_num_groups); return TRANSCRIBE_ERR_GGUF; } if (hp.bos_token_id < 0 || hp.eos_token_id < 0 || hp.pad_token_id < 0 || hp.decoder_start_token_id < 0) { - std::fprintf(stderr, "moonshine: special token IDs must be set\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine: special token IDs must be set"); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/moonshine_streaming/decoder.cpp b/src/arch/moonshine_streaming/decoder.cpp index 7eb7e645..76025646 100644 --- a/src/arch/moonshine_streaming/decoder.cpp +++ b/src/arch/moonshine_streaming/decoder.cpp @@ -23,6 +23,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -299,8 +300,8 @@ AdapterBuild build_adapter_graph(ggml_context * ctx, AdapterBuild ab {}; if (ctx == nullptr || T_enc <= 0) { - std::fprintf(stderr, - "moonshine_streaming adapter: invalid arg (ctx=%p, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming adapter: invalid arg (ctx=%p, T_enc=%d)", static_cast(ctx), T_enc); return ab; } @@ -327,8 +328,8 @@ AdapterBuild build_adapter_graph(ggml_context * ctx, // proj_w ne=[enc_h, dec_h]; result ne=[dec_h, T_enc] y = ggml_mul_mat(ctx, w.adapter.proj_w, y); } else if (enc_h != dec_h) { - std::fprintf(stderr, - "moonshine_streaming adapter: enc_h (%d) != dec_h (%d) but adapter_has_proj=false\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming adapter: enc_h (%d) != dec_h (%d) but adapter_has_proj=false", enc_h, dec_h); return ab; } @@ -339,8 +340,8 @@ AdapterBuild build_adapter_graph(ggml_context * ctx, ab.graph = ggml_new_graph_custom(ctx, 4096, false); if (ab.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming adapter: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming adapter: ggml_new_graph_custom failed"); return ab; } // Build forward through both pos_emb (for the dump) and out. @@ -363,8 +364,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || T_enc <= 0) { - std::fprintf(stderr, - "moonshine_streaming cross_kv: invalid arg (ctx=%p, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv: invalid arg (ctx=%p, T_enc=%d)", static_cast(ctx), T_enc); return db; } @@ -378,8 +379,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 4096, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming cross_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv: ggml_new_graph_custom failed"); return db; } @@ -424,9 +425,9 @@ CrossKVProjectionBuild build_cross_kv_projection_graph( CrossKVProjectionBuild pb {}; if (ctx == nullptr || n_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming cross_kv_proj: invalid arg " - "(ctx=%p, n_frames=%d)\n", + "(ctx=%p, n_frames=%d)", static_cast(ctx), n_frames); return pb; } @@ -439,8 +440,8 @@ CrossKVProjectionBuild build_cross_kv_projection_graph( pb.graph = ggml_new_graph_custom(ctx, 4096, false); if (pb.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming cross_kv_proj: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv_proj: ggml_new_graph_custom failed"); return pb; } @@ -486,9 +487,9 @@ CrossKVCommitBuild build_cross_kv_commit_graph( if (ctx == nullptr || T_enc <= 0 || kv_cache.cross_k == nullptr || kv_cache.cross_v == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming cross_kv_commit: invalid arg " - "(ctx=%p, T_enc=%d, cache=%s)\n", + "(ctx=%p, T_enc=%d, cache=%s)", static_cast(ctx), T_enc, (kv_cache.cross_k == nullptr || kv_cache.cross_v == nullptr) ? "null" : "ok"); @@ -498,8 +499,8 @@ CrossKVCommitBuild build_cross_kv_commit_graph( const int d_model = hp.dec_d_model; cb.graph = ggml_new_graph_custom(ctx, 4096, false); if (cb.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming cross_kv_commit: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv_commit: ggml_new_graph_custom failed"); return cb; } @@ -558,16 +559,16 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || n_tokens <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming decoder_kv: invalid arg " - "(ctx=%p, n_tokens=%d, T_enc=%d)\n", + "(ctx=%p, n_tokens=%d, T_enc=%d)", static_cast(ctx), n_tokens, T_enc); return db; } const int n_kv = n_past + n_tokens; if (n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "moonshine_streaming decoder_kv: n_kv=%d exceeds n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming decoder_kv: n_kv=%d exceeds n_ctx=%d", n_kv, kv_cache.n_ctx); return db; } @@ -613,8 +614,8 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming decoder_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming decoder_kv: ggml_new_graph_custom failed"); return db; } @@ -888,7 +889,7 @@ StepBuildBatched build_step_graph_batched(ggml_context * ctx, sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || T_enc_max <= 0 || n_batch <= 0) return sb; if (!use_flash) { - std::fprintf(stderr, "moonshine_streaming step(batched): requires flash path\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming step(batched): requires flash path"); return sb; } diff --git a/src/arch/moonshine_streaming/encoder.cpp b/src/arch/moonshine_streaming/encoder.cpp index a4c43715..d58d688d 100644 --- a/src/arch/moonshine_streaming/encoder.cpp +++ b/src/arch/moonshine_streaming/encoder.cpp @@ -36,6 +36,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -263,8 +264,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_samples <= 0) { - std::fprintf(stderr, - "moonshine_streaming encoder: invalid arg (ctx=%p, n_samples=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming encoder: invalid arg (ctx=%p, n_samples=%d)", static_cast(ctx), n_samples); return eb; } @@ -276,8 +277,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int T_enc = encoder_t_enc(hp, n_samples); if (T_enc <= 0) { - std::fprintf(stderr, - "moonshine_streaming encoder: input too short (n_samples=%d → T_enc=0)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming encoder: input too short (n_samples=%d → T_enc=0)", n_samples); return eb; } @@ -287,9 +288,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // marking the pad slots. For Stage 4 jfk we use the trimmed length // and require an exact multiple. if (n_samples % frame_len != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming encoder: n_samples=%d not a multiple of " - "frame_len=%d (caller must right-pad before calling)\n", + "frame_len=%d (caller must right-pad before calling)", n_samples, frame_len); return eb; } @@ -417,8 +418,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, 8192, false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "moonshine_streaming encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/moonshine_streaming/model.cpp b/src/arch/moonshine_streaming/model.cpp index 71a7ff0e..19e40615 100644 --- a/src/arch/moonshine_streaming/model.cpp +++ b/src/arch/moonshine_streaming/model.cpp @@ -138,9 +138,9 @@ bool kv_cache_init(MoonshineStreamingKvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming kv_cache: unsupported kv_type=%d " - "(only F16/F32)\n", static_cast(kv_type)); + "(only F16/F32)", static_cast(kv_type)); return false; } @@ -148,7 +148,7 @@ bool kv_cache_init(MoonshineStreamingKvCache & cache, ggml_init_params params { ctx_size, nullptr, /*no_alloc=*/true }; cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "moonshine_streaming kv_cache: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming kv_cache: ggml_init failed"); return false; } @@ -167,7 +167,7 @@ bool kv_cache_init(MoonshineStreamingKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "moonshine_streaming kv_cache: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming kv_cache: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -199,7 +199,7 @@ bool kv_cache_init_batched(MoonshineStreamingKvCache & cache, return true; } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, "moonshine_streaming kv_cache(batched): unsupported kv_type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming kv_cache(batched): unsupported kv_type"); return false; } const size_t ctx_size = 4 * ggml_tensor_overhead() + 256; @@ -298,8 +298,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "moonshine_streaming: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -412,8 +412,8 @@ transcribe_status ensure_sched(MoonshineStreamingSession * cc, static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "moonshine_streaming: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_OK; @@ -493,9 +493,9 @@ transcribe_status encode_window_to_host( if (n_samples <= 0 || hp.enc_frame_len <= 0 || n_samples % hp.enc_frame_len != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming encode_window: invalid n_samples=%d " - "(frame_len=%d)\n", n_samples, hp.enc_frame_len); + "(frame_len=%d)", n_samples, hp.enc_frame_len); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -541,8 +541,8 @@ transcribe_status encode_window_to_host( apply_thread_policy(cc); if (ggml_backend_sched_graph_compute(cc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming encode_window: encoder compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming encode_window: encoder compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -633,8 +633,8 @@ transcribe_status apply_adapter_window( pos_ids.size() * sizeof(int32_t)); if (ggml_backend_sched_graph_compute(cc->sched, ab.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming adapter: compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming adapter: compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -681,9 +681,9 @@ transcribe_status project_cross_kv_window( if (static_cast(out_per_layer_k.size()) != n_layers || static_cast(out_per_layer_v.size()) != n_layers) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming cross_kv_proj: per-layer buffer " - "size mismatch (got K=%zu V=%zu, expected %d)\n", + "size mismatch (got K=%zu V=%zu, expected %d)", out_per_layer_k.size(), out_per_layer_v.size(), n_layers); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -717,8 +717,8 @@ transcribe_status project_cross_kv_window( if (ggml_backend_sched_graph_compute(cc->sched, pb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming cross_kv_proj: compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv_proj: compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -813,9 +813,9 @@ transcribe_status commit_cross_kv_from_host( if (per_layer_k[il].size() != expected_floats || per_layer_v[il].size() != expected_floats) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming cross_kv_commit: layer %d size " - "mismatch (K=%zu V=%zu, expected %zu)\n", + "mismatch (K=%zu V=%zu, expected %zu)", il, per_layer_k[il].size(), per_layer_v[il].size(), expected_floats); return TRANSCRIBE_ERR_INVALID_ARG; @@ -854,8 +854,8 @@ transcribe_status commit_cross_kv_from_host( if (ggml_backend_sched_graph_compute(cc->sched, cb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming cross_kv_commit: compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming cross_kv_commit: compute failed"); return TRANSCRIBE_ERR_GGUF; } cc->kv_cache.cross_populated = true; @@ -956,8 +956,8 @@ transcribe_status decode_from_kv_cache( if (ggml_backend_sched_graph_compute(cc->sched, db.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming decode: decoder compute failed (n_past=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming decode: decoder compute failed (n_past=%d)", n_past_in); return TRANSCRIBE_ERR_GGUF; } @@ -1171,8 +1171,8 @@ transcribe_status decode_from_committed_enc( if (ggml_backend_sched_graph_compute(cc->sched, cross_db.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "moonshine_streaming decode: cross_kv compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming decode: cross_kv compute failed"); return TRANSCRIBE_ERR_GGUF; } cc->kv_cache.cross_populated = true; @@ -1608,9 +1608,9 @@ transcribe_status flush_stable_frames( const int64_t abs_pcm_start = static_cast(slice_start_frame) * spf; const int64_t abs_pcm_end = static_cast(slice_end_frame) * spf; if (abs_pcm_start < cc->stream_pcm_start_sample) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming flush: slice underruns trimmed " - "PCM (slice_start_abs=%lld, pcm_start=%lld)\n", + "PCM (slice_start_abs=%lld, pcm_start=%lld)", static_cast(abs_pcm_start), static_cast(cc->stream_pcm_start_sample)); return TRANSCRIBE_ERR_GGUF; @@ -1639,9 +1639,9 @@ transcribe_status flush_stable_frames( } const int expected_T = slice_end_frame - slice_start_frame; if (slice_T_enc != expected_T) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming flush: slice T_enc mismatch " - "(got %d, expected %d)\n", slice_T_enc, expected_T); + "(got %d, expected %d)", slice_T_enc, expected_T); return TRANSCRIBE_ERR_GGUF; } @@ -2288,9 +2288,9 @@ transcribe_status run_batch( } if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "moonshine_streaming run_batch: n=%d T_enc_max=%d kv_window=%d (cap %d)\n" - " enc+adapter=%.1fms (serial x%d) decode=%.1fms (batched)\n", + " enc+adapter=%.1fms (serial x%d) decode=%.1fms (batched)", n, T_enc_max, kv_window, n_ctx_cap, enc_us / 1000.0, n, dec_us / 1000.0); } return TRANSCRIBE_OK; diff --git a/src/arch/moonshine_streaming/weights.cpp b/src/arch/moonshine_streaming/weights.cpp index 02c93dd9..0d988262 100644 --- a/src/arch/moonshine_streaming/weights.cpp +++ b/src/arch/moonshine_streaming/weights.cpp @@ -23,6 +23,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -54,7 +55,7 @@ transcribe_status read_optional_u32_kv(const gguf_context * gguf, out = default_value; return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -74,7 +75,7 @@ transcribe_status read_optional_f32_kv(const gguf_context * gguf, out = default_value; return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -86,17 +87,17 @@ transcribe_status read_required_u32_array_kv(const gguf_context * gguf, if (gguf == nullptr) return TRANSCRIBE_ERR_INVALID_ARG; const int64_t kid = gguf_find_key(gguf, key); if (kid < 0) { - std::fprintf(stderr, "%s: missing required KV %s\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: missing required KV %s", error_tag, key); return TRANSCRIBE_ERR_GGUF; } if (gguf_get_kv_type(gguf, kid) != GGUF_TYPE_ARRAY) { - std::fprintf(stderr, "%s: KV %s is not an array\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s is not an array", error_tag, key); return TRANSCRIBE_ERR_GGUF; } const gguf_type elem_type = gguf_get_arr_type(gguf, kid); if (elem_type != GGUF_TYPE_UINT32 && elem_type != GGUF_TYPE_INT32) { - std::fprintf(stderr, - "%s: KV %s array element type is not u32/i32 (got %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: KV %s array element type is not u32/i32 (got %d)", error_tag, key, static_cast(elem_type)); return TRANSCRIBE_ERR_GGUF; } @@ -191,7 +192,7 @@ transcribe_status read_moonshine_streaming_hparams(const gguf_context * if (hp.enc_n_layers <= 0 || hp.enc_d_model <= 0 || hp.enc_n_heads <= 0 || hp.enc_head_dim <= 0 || hp.enc_ffn_dim <= 0) { - std::fprintf(stderr, "moonshine_streaming: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } // Encoder allows enc_d_model > n_heads * head_dim (small: 620 vs 512; @@ -201,8 +202,8 @@ transcribe_status read_moonshine_streaming_hparams(const gguf_context * // the residual stream dim in any in-the-wild moonshine_streaming // variant; flag the inverted case to catch a malformed config early. if (hp.enc_d_model < hp.enc_n_heads * hp.enc_head_dim) { - std::fprintf(stderr, - "moonshine_streaming: encoder d_model (%d) < n_heads (%d) * head_dim (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: encoder d_model (%d) < n_heads (%d) * head_dim (%d)", hp.enc_d_model, hp.enc_n_heads, hp.enc_head_dim); return TRANSCRIBE_ERR_GGUF; } @@ -210,80 +211,80 @@ transcribe_status read_moonshine_streaming_hparams(const gguf_context * hp.dec_head_dim <= 0 || hp.dec_ffn_dim <= 0 || hp.dec_max_position_embeddings <= 0 || hp.dec_vocab_size <= 0) { - std::fprintf(stderr, "moonshine_streaming: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_d_model != hp.dec_n_heads * hp.dec_head_dim) { - std::fprintf(stderr, - "moonshine_streaming: decoder d_model (%d) != n_heads (%d) * head_dim (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: decoder d_model (%d) != n_heads (%d) * head_dim (%d)", hp.dec_d_model, hp.dec_n_heads, hp.dec_head_dim); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_activation != "gelu") { - std::fprintf(stderr, - "moonshine_streaming: only \"gelu\" encoder activation supported (got \"%s\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: only \"gelu\" encoder activation supported (got \"%s\")", hp.enc_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_activation != "silu") { - std::fprintf(stderr, - "moonshine_streaming: only \"silu\" decoder activation supported (got \"%s\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: only \"silu\" decoder activation supported (got \"%s\")", hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_tie_word_embeddings) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming: tie_word_embeddings=true is not supported " - "(reference always carries a separate proj_out.weight)\n"); + "(reference always carries a separate proj_out.weight)"); return TRANSCRIBE_ERR_GGUF; } if (hp.attention_bias) { - std::fprintf(stderr, - "moonshine_streaming: attention_bias=true is not supported\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: attention_bias=true is not supported"); return TRANSCRIBE_ERR_GGUF; } if (hp.partial_rotary_factor <= 0.0f || hp.partial_rotary_factor > 1.0f) { - std::fprintf(stderr, - "moonshine_streaming: invalid partial_rotary_factor=%g (expected (0, 1])\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: invalid partial_rotary_factor=%g (expected (0, 1])", hp.partial_rotary_factor); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "raw") { - std::fprintf(stderr, - "moonshine_streaming: unsupported frontend type \"%s\" (only \"raw\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: unsupported frontend type \"%s\" (only \"raw\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_sample_rate != 16000) { - std::fprintf(stderr, - "moonshine_streaming: unsupported sample_rate=%d (only 16000 Hz)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: unsupported sample_rate=%d (only 16000 Hz)", hp.fe_sample_rate); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_frame_len <= 0) { - std::fprintf(stderr, - "moonshine_streaming: invalid encoder frame_len=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: invalid encoder frame_len=%d", hp.enc_frame_len); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_sliding_windows.size() != static_cast(hp.enc_n_layers) * 2) { - std::fprintf(stderr, - "moonshine_streaming: encoder.sliding_windows length %zu != 2 * n_layers (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: encoder.sliding_windows length %zu != 2 * n_layers (%d)", hp.enc_sliding_windows.size(), hp.enc_n_layers); return TRANSCRIBE_ERR_GGUF; } if (hp.encoder_hidden_size != hp.enc_d_model) { - std::fprintf(stderr, - "moonshine_streaming: encoder_hidden_size (%d) disagrees with encoder.d_model (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "moonshine_streaming: encoder_hidden_size (%d) disagrees with encoder.d_model (%d)", hp.encoder_hidden_size, hp.enc_d_model); return TRANSCRIBE_ERR_GGUF; } if (hp.bos_token_id < 0 || hp.eos_token_id < 0 || hp.pad_token_id < 0 || hp.decoder_start_token_id < 0) { - std::fprintf(stderr, "moonshine_streaming: special token IDs must be set\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "moonshine_streaming: special token IDs must be set"); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/parakeet/decoder.cpp b/src/arch/parakeet/decoder.cpp index 55f0566f..0e919fcc 100644 --- a/src/arch/parakeet/decoder.cpp +++ b/src/arch/parakeet/decoder.cpp @@ -18,6 +18,7 @@ #include "weights.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-backend.h" @@ -77,19 +78,19 @@ bool read_tensor_to_f32(const ggml_tensor * t, std::vector & out) { if (t == nullptr) { - std::fprintf(stderr, "parakeet decoder: read_tensor_to_f32: null tensor\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: read_tensor_to_f32: null tensor"); return false; } const size_t nbytes = ggml_nbytes(t); if (nbytes == 0) { - std::fprintf(stderr, - "parakeet decoder: tensor \"%s\" has 0 bytes\n", t->name); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: tensor \"%s\" has 0 bytes", t->name); return false; } const int64_t nelem = ggml_nelements(t); if (nelem <= 0) { - std::fprintf(stderr, - "parakeet decoder: tensor \"%s\" has nelem=%lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: tensor \"%s\" has nelem=%lld", t->name, static_cast(nelem)); return false; } @@ -102,9 +103,9 @@ bool read_tensor_to_f32(const ggml_tensor * t, // Read straight from the backend into the output buffer. if (t->type == GGML_TYPE_F32) { if (nbytes != static_cast(nelem) * sizeof(float)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: tensor \"%s\" f32 nbytes %zu " - "!= nelem*sizeof(float) %zu\n", + "!= nelem*sizeof(float) %zu", t->name, nbytes, static_cast(nelem) * sizeof(float)); return false; @@ -119,8 +120,8 @@ bool read_tensor_to_f32(const ggml_tensor * t, // table. const auto * tt = ggml_get_type_traits(t->type); if (tt == nullptr || tt->to_float == nullptr) { - std::fprintf(stderr, - "parakeet decoder: tensor \"%s\" type %s has no to_float\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: tensor \"%s\" type %s has no to_float", t->name, ggml_type_name(t->type)); return false; } @@ -168,7 +169,7 @@ bool build_joint_weight(HostJoint & j, const ggml_tensor * src_out_w, // carries no per-step state and is safe to keep on the shared model. j.w_backend = ggml_backend_cpu_init(); if (j.w_backend == nullptr) { - std::fprintf(stderr, "parakeet decoder: ggml CPU backend init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: ggml CPU backend init failed"); return fail(); } @@ -340,16 +341,16 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, static_cast(out.ctc_head.n_classes) * static_cast(out.ctc_head.d_enc); if (out.ctc_head.weight.size() != expected_w) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: head.ctc.weight size %zu " - "!= n_classes*d_enc %zu\n", + "!= n_classes*d_enc %zu", out.ctc_head.weight.size(), expected_w); return TRANSCRIBE_ERR_GGUF; } if (static_cast(out.ctc_head.bias.size()) != out.ctc_head.n_classes) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: head.ctc.bias size %zu " - "!= n_classes %d\n", + "!= n_classes %d", out.ctc_head.bias.size(), out.ctc_head.n_classes); return TRANSCRIBE_ERR_GGUF; } @@ -376,9 +377,9 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, const size_t expected = static_cast(hp.pred_vocab) * static_cast(hp.pred_hidden); if (out.predictor.embed_w.size() != expected) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: pred.embed.weight size %zu " - "!= pred_vocab*pred_hidden %zu\n", + "!= pred_vocab*pred_hidden %zu", out.predictor.embed_w.size(), expected); return TRANSCRIBE_ERR_GGUF; } @@ -395,13 +396,13 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, const size_t gates = static_cast(4 * hp.pred_hidden); const size_t mat_size = gates * static_cast(hp.pred_hidden); if (lh.Wx.size() != mat_size || lh.Wh.size() != mat_size) { - std::fprintf(stderr, - "parakeet decoder: pred.lstm.%d Wx/Wh wrong size\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: pred.lstm.%d Wx/Wh wrong size", i); return TRANSCRIBE_ERR_GGUF; } if (lh.b.size() != gates) { - std::fprintf(stderr, - "parakeet decoder: pred.lstm.%d bias wrong size\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: pred.lstm.%d bias wrong size", i); return TRANSCRIBE_ERR_GGUF; } } @@ -428,9 +429,9 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, const int ne0 = static_cast(w.joint.out_w->ne[0]); const int ne1 = static_cast(w.joint.out_w->ne[1]); if (ne0 != out.joint.joint_h || ne1 != out.joint.joint_n) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: out_w ne [%d,%d] != [joint_h=%d, joint_n=%d]; " - "using host matmul\n", + "using host matmul", ne0, ne1, out.joint.joint_h, out.joint.joint_n); } else { const bool force_fp32 = std::getenv("TRANSCRIBE_JOINT_FP32") != nullptr; @@ -438,16 +439,16 @@ transcribe_status build_host_decoder_weights(const ParakeetModel & model, if (!force_fp32) { ok = build_joint_weight(out.joint, w.joint.out_w, /*use_native=*/true); if (!ok) { - std::fprintf(stderr, - "parakeet decoder: native joint weight failed; retrying fp32\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: native joint weight failed; retrying fp32"); } } if (!ok) { ok = build_joint_weight(out.joint, w.joint.out_w, /*use_native=*/false); } if (!ok) { - std::fprintf(stderr, - "parakeet decoder: joint ggml weight build failed; using host matmul\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder: joint ggml weight build failed; using host matmul"); } } } @@ -890,9 +891,9 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, return TRANSCRIBE_ERR_INVALID_ARG; } if (d_enc != w.joint.d_enc) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: enc d_model mismatch (got %d, " - "expected %d)\n", d_enc, w.joint.d_enc); + "expected %d)", d_enc, w.joint.d_enc); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1078,10 +1079,10 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, } } - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "decoder: %d iters, %d tokens, T_enc=%d " "enc_proj=%.1f ms pred=%.1f ms joint=%.1f ms conf=%.1f ms " - "total=%.1f ms per_iter=%.0f us\n", + "total=%.1f ms per_iter=%.0f us", iter, static_cast(out_tokens.size()), T_enc, t_enc_proj_us / 1000.0, t_pred_us / 1000.0, @@ -1091,9 +1092,9 @@ transcribe_status decode_tdt_greedy(const HostDecoderWeights & w, static_cast(t_pred_us + t_joint_us) / std::max(iter, 1)); if (iter >= max_iters) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: hit iteration cap (%d) — pathological " - "logits or loop bug\n", max_iters); + "logits or loop bug", max_iters); return TRANSCRIBE_ERR_BACKEND; } return TRANSCRIBE_OK; @@ -1134,9 +1135,9 @@ transcribe_status decode_rnnt_greedy(const HostDecoderWeights & w, return TRANSCRIBE_ERR_INVALID_ARG; } if (d_enc != w.joint.d_enc) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder (rnnt): enc d_model mismatch (got %d, " - "expected %d)\n", d_enc, w.joint.d_enc); + "expected %d)", d_enc, w.joint.d_enc); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1267,10 +1268,10 @@ transcribe_status decode_rnnt_greedy(const HostDecoderWeights & w, } } - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "decoder (rnnt): %d iters, %d tokens, T_enc=%d " "enc_proj=%.1f ms pred=%.1f ms joint=%.1f ms conf=%.1f ms " - "total=%.1f ms\n", + "total=%.1f ms", iter, static_cast(out_tokens.size()), T_enc, t_enc_proj_us / 1000.0, t_pred_us / 1000.0, @@ -1279,9 +1280,9 @@ transcribe_status decode_rnnt_greedy(const HostDecoderWeights & w, (t_enc_proj_us + t_pred_us + t_joint_us + t_conf_us) / 1000.0); if (iter >= max_iters) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder (rnnt): hit iteration cap (%d) — " - "pathological logits or loop bug\n", max_iters); + "pathological logits or loop bug", max_iters); return TRANSCRIBE_ERR_BACKEND; } return TRANSCRIBE_OK; @@ -1318,16 +1319,16 @@ transcribe_status decode_rnnt_greedy_streaming( // a hypothetical future TDT/CTC streaming model — but it fails loud at // the exact point of mis-decode rather than emitting wrong tokens. if (w.head_kind != HostHeadKind::RNNT) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder (rnnt-stream): streaming decode " "requires an RNN-T head; this model's head is not " - "RNN-T\n"); + "RNN-T"); return TRANSCRIBE_ERR_NOT_IMPLEMENTED; } if (d_enc != w.joint.d_enc) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder (rnnt-stream): enc d_model mismatch " - "(got %d, expected %d)\n", d_enc, w.joint.d_enc); + "(got %d, expected %d)", d_enc, w.joint.d_enc); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1436,8 +1437,8 @@ transcribe_status decode_rnnt_greedy_streaming( } if (iter >= max_iters) { - std::fprintf(stderr, - "parakeet decoder (rnnt-stream): hit iteration cap (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet decoder (rnnt-stream): hit iteration cap (%d)", max_iters); return TRANSCRIBE_ERR_BACKEND; } @@ -1473,9 +1474,9 @@ transcribe_status decode_ctc_greedy(const HostDecoderWeights & w, return TRANSCRIBE_ERR_INVALID_ARG; } if (d_enc != w.ctc_head.d_enc) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder (ctc): enc d_model mismatch " - "(got %d, expected %d)\n", d_enc, w.ctc_head.d_enc); + "(got %d, expected %d)", d_enc, w.ctc_head.d_enc); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1562,8 +1563,8 @@ transcribe_status decode_ctc_greedy(const HostDecoderWeights & w, out_tokens.push_back(tok); } - std::fprintf(stderr, - "decoder (ctc): T_enc=%d proj=%.1f ms emitted=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "decoder (ctc): T_enc=%d proj=%.1f ms emitted=%d", T_enc, t_proj_us / 1000.0, static_cast(out_tokens.size())); diff --git a/src/arch/parakeet/encoder.cpp b/src/arch/parakeet/encoder.cpp index 1664ffd0..30a1fc64 100644 --- a/src/arch/parakeet/encoder.cpp +++ b/src/arch/parakeet/encoder.cpp @@ -46,6 +46,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -300,9 +301,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet encoder: invalid arg " - "(ctx=%p, n_mel_frames=%d)\n", + "(ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } @@ -314,16 +315,16 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // factor != 8, build_pre_encode would need a structural rework, so // we fail loudly here. if (hp.enc_subsampling_factor != 8) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet encoder: unsupported subsampling_factor=%d " - "(only 8 implemented)\n", + "(only 8 implemented)", hp.enc_subsampling_factor); return eb; } if (hp.fe_num_mels <= 0 || (hp.fe_num_mels % hp.enc_subsampling_factor) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet encoder: n_mels=%d not divisible by " - "subsampling_factor=%d\n", + "subsampling_factor=%d", hp.fe_num_mels, hp.enc_subsampling_factor); return eb; } @@ -337,8 +338,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.mel_in = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, n_mel_frames, hp.fe_num_mels, 1, n_batch); if (eb.mel_in == nullptr) { - std::fprintf(stderr, - "parakeet encoder: failed to allocate mel_in tensor\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet encoder: failed to allocate mel_in tensor"); return eb; } ggml_set_name(eb.mel_in, "mel.in"); @@ -660,9 +661,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, ggml_tensor * one_hot = ggml_new_tensor_4d( ctx, GGML_TYPE_F32, P, T_enc_val, B, 1); if (one_hot == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet encoder: failed to allocate " - "prompt.one_hot.in tensor\n"); + "prompt.one_hot.in tensor"); return eb; } ggml_set_name(one_hot, "prompt.one_hot.in"); @@ -701,8 +702,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // 2200. Use 8192 to leave headroom. eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "parakeet encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -766,26 +767,26 @@ EncoderBuild build_encoder_graph_streaming( EncoderBuild eb {}; if (ctx == nullptr || n_mel_chunk_frames <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: invalid arg " - "(ctx=%p, n_mel_chunk_frames=%d)\n", + "(ctx=%p, n_mel_chunk_frames=%d)", static_cast(ctx), n_mel_chunk_frames); return eb; } if (hp.enc_subsampling_factor != 8) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: unsupported " - "subsampling_factor=%d (only 8 implemented)\n", + "subsampling_factor=%d (only 8 implemented)", hp.enc_subsampling_factor); return eb; } if (hp.enc_att_context_style != ParakeetHParams::AttContextStyle::ChunkedLimited) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: requires " - "att_context_style=ChunkedLimited\n"); + "att_context_style=ChunkedLimited"); return eb; } @@ -793,10 +794,10 @@ EncoderBuild build_encoder_graph_streaming( if (static_cast(cache_io.channel_in.size()) != n_layers || static_cast(cache_io.time_in.size()) != n_layers) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: cache_io.in vectors " "must be sized to n_layers=%d (got channel=%zu, " - "time=%zu)\n", + "time=%zu)", n_layers, cache_io.channel_in.size(), cache_io.time_in.size()); return eb; @@ -826,9 +827,9 @@ EncoderBuild build_encoder_graph_streaming( if (drop_extra_pre_encoded > 0) { const int64_t T_pre = x->ne[1]; if (drop_extra_pre_encoded >= T_pre) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: drop_extra=%d >= " - "T_pre_enc=%lld\n", + "T_pre_enc=%lld", drop_extra_pre_encoded, (long long)T_pre); return eb; } @@ -943,9 +944,9 @@ EncoderBuild build_encoder_graph_streaming( ggml_tensor * one_hot = ggml_new_tensor_4d( ctx, GGML_TYPE_F32, P, T_q, 1, 1); if (one_hot == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet streaming encoder: failed to allocate " - "prompt.one_hot.in tensor\n"); + "prompt.one_hot.in tensor"); return eb; } ggml_set_name(one_hot, "prompt.one_hot.in"); diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp index 5ac0336f..d00af67b 100644 --- a/src/arch/parakeet/model.cpp +++ b/src/arch/parakeet/model.cpp @@ -51,6 +51,7 @@ #include "transcribe-loader.h" #include "transcribe-mel.h" #include "transcribe-meta.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-alloc.h" @@ -377,9 +378,9 @@ transcribe_status init_streaming_caches(ParakeetSession * pc, : (hp.enc_conv_kernel - 1); if (n_layer <= 0 || d_model <= 0 || T_cache <= 0 || k_minus_1 <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream init_caches: degenerate sizes " - "(n_layer=%d, d_model=%d, T_cache=%d, k-1=%d)\n", + "(n_layer=%d, d_model=%d, T_cache=%d, k-1=%d)", n_layer, d_model, T_cache, k_minus_1); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -394,8 +395,8 @@ transcribe_status init_streaming_caches(ParakeetSession * pc, ip.no_alloc = true; pc->stream_caches.ctx = ggml_init(ip); if (pc->stream_caches.ctx == nullptr) { - std::fprintf(stderr, - "parakeet stream init_caches: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet stream init_caches: ggml_init failed"); return TRANSCRIBE_ERR_OOM; } @@ -425,8 +426,8 @@ transcribe_status init_streaming_caches(ParakeetSession * pc, pc->stream_caches.buffer = ggml_backend_alloc_ctx_tensors(pc->stream_caches.ctx, pm->plan.primary); if (pc->stream_caches.buffer == nullptr) { - std::fprintf(stderr, - "parakeet stream init_caches: backend buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet stream init_caches: backend buffer alloc failed"); ggml_free(pc->stream_caches.ctx); pc->stream_caches.ctx = nullptr; return TRANSCRIBE_ERR_BACKEND; @@ -707,8 +708,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "parakeet: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -1190,8 +1191,8 @@ static transcribe_status run_one_shot_inner( // on the model. compute() is documented thread-safe across // contexts since the instance is const-after-construction. if (!pm->mel.has_value()) { - std::fprintf(stderr, - "parakeet run: model has no MelFrontend (load skipped?)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: model has no MelFrontend (load skipped?)"); return TRANSCRIBE_ERR_INVALID_ARG; } const int64_t t_mel_start = ggml_time_us(); @@ -1202,8 +1203,8 @@ static transcribe_status run_one_shot_inner( pc->mel_buf, mel_n_mels, mel_n_frames); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "parakeet run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -1239,8 +1240,8 @@ static transcribe_status run_one_shot_inner( init_params.no_alloc = true; pc->compute_ctx = ggml_init(init_params); if (pc->compute_ctx == nullptr) { - std::fprintf(stderr, - "parakeet run: ggml_init for compute_ctx failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: ggml_init for compute_ctx failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -1271,15 +1272,15 @@ static transcribe_status run_one_shot_inner( static_cast(pm->plan.scheduler_list.size()), /*graph_size=*/8192, /*parallel=*/false, /*op_offload=*/true); if (pc->sched == nullptr) { - std::fprintf(stderr, - "parakeet run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } ggml_backend_sched_reset(pc->sched); if (!ggml_backend_sched_alloc_graph(pc->sched, eb.graph)) { - std::fprintf(stderr, - "parakeet run: ggml_backend_sched_alloc_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: ggml_backend_sched_alloc_graph failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1309,9 +1310,9 @@ static transcribe_status run_one_shot_inner( (params != nullptr) ? params->language : nullptr; const int32_t pid = resolve_prompt_id(pm->hparams, lang_hint); if (pid < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run: language %s%s%s not in prompt " - "dictionary\n", + "dictionary", lang_hint ? "\"" : "", lang_hint ? lang_hint : "", lang_hint ? "\"" : ""); @@ -1321,9 +1322,9 @@ static transcribe_status run_one_shot_inner( if (!fill_prompt_one_hot(one_hot_buf, P, T_oh, /*n_batch=*/1, {pid})) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run: prompt_id %d out of range " - "[0, %d)\n", pid, P); + "[0, %d)", pid, P); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_set(eb.prompt_one_hot_in, one_hot_buf.data(), @@ -1469,8 +1470,8 @@ static transcribe_status run_one_shot_inner( ggml_backend_sched_graph_compute(pc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "parakeet run: ggml_backend_sched_graph_compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet run: ggml_backend_sched_graph_compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1564,9 +1565,9 @@ static transcribe_status run_one_shot_inner( const int d_enc = static_cast(eb.out->ne[0]); const int T_enc = static_cast(eb.out->ne[1]); if (d_enc <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run: encoder output has degenerate shape " - "[%d, %d]\n", d_enc, T_enc); + "[%d, %d]", d_enc, T_enc); return TRANSCRIBE_ERR_GGUF; } pc->enc_host.resize(static_cast(d_enc) * @@ -1810,9 +1811,9 @@ static transcribe_status run_batch_encode( (params != nullptr) ? params->language : nullptr; const int32_t pid = resolve_prompt_id(pm->hparams, lang_hint); if (pid < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run_batch: language %s%s%s not in prompt " - "dictionary\n", + "dictionary", lang_hint ? "\"" : "", lang_hint ? lang_hint : "", lang_hint ? "\"" : ""); @@ -1821,9 +1822,9 @@ static transcribe_status run_batch_encode( std::vector pids(static_cast(n), pid); std::vector one_hot_buf; if (!fill_prompt_one_hot(one_hot_buf, P, T_oh, /*n_batch=*/n, pids)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run_batch: prompt_id %d out of range " - "[0, %d)\n", pid, P); + "[0, %d)", pid, P); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_set(eb.prompt_one_hot_in, one_hot_buf.data(), @@ -1880,7 +1881,7 @@ static transcribe_status run_batch_encode( ggml_backend_sched_graph_compute(pc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "parakeet run_batch: graph_compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet run_batch: graph_compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -2251,7 +2252,7 @@ transcribe_status emit_streaming_chunk( ip.no_alloc = true; pc->compute_ctx = ggml_init(ip); if (pc->compute_ctx == nullptr) { - std::fprintf(stderr, "parakeet stream: ggml_init compute_ctx failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: ggml_init compute_ctx failed"); return TRANSCRIBE_ERR_OOM; } @@ -2312,13 +2313,13 @@ transcribe_status emit_streaming_chunk( static_cast(pm->plan.scheduler_list.size()), /*graph_size=*/8192, /*parallel=*/false, /*op_offload=*/true); if (pc->sched == nullptr) { - std::fprintf(stderr, "parakeet stream: sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: sched_new failed"); return TRANSCRIBE_ERR_BACKEND; } } ggml_backend_sched_reset(pc->sched); if (!ggml_backend_sched_alloc_graph(pc->sched, eb.graph)) { - std::fprintf(stderr, "parakeet stream: alloc_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: alloc_graph failed"); return TRANSCRIBE_ERR_BACKEND; } @@ -2360,9 +2361,9 @@ transcribe_status emit_streaming_chunk( const char * lang_hint = pc->stream_run_params.language; const int32_t pid = resolve_prompt_id(pm->hparams, lang_hint); if (pid < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: language %s%s%s not in prompt " - "dictionary\n", + "dictionary", lang_hint ? "\"" : "", lang_hint ? lang_hint : "", lang_hint ? "\"" : ""); @@ -2372,9 +2373,9 @@ transcribe_status emit_streaming_chunk( if (!fill_prompt_one_hot(one_hot_buf, P, T_oh, /*n_batch=*/1, {pid})) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: prompt_id %d out of range " - "[0, %d)\n", pid, P); + "[0, %d)", pid, P); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_set(eb.prompt_one_hot_in, one_hot_buf.data(), @@ -2403,8 +2404,8 @@ transcribe_status emit_streaming_chunk( ggml_backend_sched_graph_compute(pc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "parakeet stream: graph_compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet stream: graph_compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -2466,9 +2467,9 @@ transcribe_status emit_streaming_chunk( if (cache_io.channel_out[i] == nullptr || cache_io.channel_out[i]->buffer == nullptr || cache_io.time_out[i] == nullptr || cache_io.time_out[i]->buffer == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet stream: cache_out unallocated at layer %d " - "(att_context_right=%d) — builder bug\n", + "(att_context_right=%d) — builder bug", i, pc->stream_caches.att_context_right); return TRANSCRIBE_ERR_BACKEND; } @@ -2629,8 +2630,8 @@ transcribe_status emit_buffered_chunk( const auto & hp = pm->hparams; if (!pm->mel.has_value()) { - std::fprintf(stderr, - "parakeet buffered: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -2691,8 +2692,8 @@ transcribe_status emit_buffered_chunk( pc->mel_buf, mel_n_mels, mel_n_frames); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "parakeet buffered: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -2711,8 +2712,8 @@ transcribe_status emit_buffered_chunk( init_params.no_alloc = true; pc->compute_ctx = ggml_init(init_params); if (pc->compute_ctx == nullptr) { - std::fprintf(stderr, - "parakeet buffered: ggml_init for compute_ctx failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: ggml_init for compute_ctx failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -2853,8 +2854,8 @@ transcribe_status emit_buffered_chunk( ggml_backend_sched_graph_compute(pc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "parakeet buffered: sched_graph_compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: sched_graph_compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -2917,8 +2918,8 @@ transcribe_status emit_buffered_chunk( const int d_enc = static_cast(eb.out->ne[0]); const int T_enc_full = static_cast(eb.out->ne[1]); if (T_enc_full <= 0) { - std::fprintf(stderr, - "parakeet buffered: encoder produced %d frames\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: encoder produced %d frames", T_enc_full); return TRANSCRIBE_ERR_GGUF; } @@ -3058,12 +3059,12 @@ transcribe_status resolve_buffered_stream_geom( !ms_to_exact_frames(req_C_ms, default_C, &C_frames) || !ms_to_exact_frames(req_R_ms, default_R, &R_frames)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet buffered: requested (L, C, R)_ms = " "(%d, %d, %d) is invalid. Use -1 on any field to " "select the model default; otherwise the value " "must be 0 or a positive exact multiple of the " - "%d ms encoder frame.\n", + "%d ms encoder frame.", req_L_ms, req_C_ms, req_R_ms, safe_frame_ms); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -3076,16 +3077,16 @@ transcribe_status resolve_buffered_stream_geom( !contains(pm->hparams.enc_att_chunk_chunk_choices, C_frames) || !contains(pm->hparams.enc_att_chunk_right_choices, R_frames)) { - std::fprintf(stderr, - "parakeet buffered: requested (L, C, R) = (%d, %d, %d) " - "encoder frames not in model menu. " - "Allowed L=", L_frames, C_frames, R_frames); - for (auto v : pm->hparams.enc_att_chunk_left_choices) std::fprintf(stderr, "%d,", v); - std::fprintf(stderr, " C="); - for (auto v : pm->hparams.enc_att_chunk_chunk_choices) std::fprintf(stderr, "%d,", v); - std::fprintf(stderr, " R="); - for (auto v : pm->hparams.enc_att_chunk_right_choices) std::fprintf(stderr, "%d,", v); - std::fprintf(stderr, "\n"); + std::string allowed = "L="; + for (auto v : pm->hparams.enc_att_chunk_left_choices) allowed += std::to_string(v) + ","; + allowed += " C="; + for (auto v : pm->hparams.enc_att_chunk_chunk_choices) allowed += std::to_string(v) + ","; + allowed += " R="; + for (auto v : pm->hparams.enc_att_chunk_right_choices) allowed += std::to_string(v) + ","; + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet buffered: requested (L, C, R) = (%d, %d, %d) " + "encoder frames not in model menu. Allowed %s", + L_frames, C_frames, R_frames, allowed.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } if (C_frames < 1) return TRANSCRIBE_ERR_INVALID_ARG; @@ -3122,10 +3123,10 @@ transcribe_status resolve_cache_aware_stream_geom( const auto * px = reinterpret_cast(family); const int requested = px->att_context_right; if (requested < -1) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: att_context_right=%d is invalid; " "use -1 for the model default or >=0 to pick " - "an entry from the model's training menu\n", + "an entry from the model's training menu", requested); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -3140,14 +3141,14 @@ transcribe_status resolve_cache_aware_stream_geom( } } if (!matched) { - std::fprintf(stderr, - "parakeet: requested att_context_right=%d " - "not in model's training menu; available: ", - requested); + std::string available; for (const auto & p : pm->hparams.enc_att_context_size_choices) { - std::fprintf(stderr, "%d ", p.second); + available += std::to_string(p.second) + " "; } - std::fprintf(stderr, "\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: requested att_context_right=%d " + "not in model's training menu; available: %s", + requested, available.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } } diff --git a/src/arch/parakeet/weights.cpp b/src/arch/parakeet/weights.cpp index ec1a36e1..c1803e53 100644 --- a/src/arch/parakeet/weights.cpp +++ b/src/arch/parakeet/weights.cpp @@ -18,6 +18,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -86,9 +87,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, switch (read_int32_array_kv(gguf, "stt.parakeet.encoder.att_context_size_choices", flat)) { case KvResult::Ok: if (flat.empty() || (flat.size() % 2) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.encoder.att_context_size_choices " - "has odd length %zu (expected pairs)\n", flat.size()); + "has odd length %zu (expected pairs)", flat.size()); return TRANSCRIBE_ERR_GGUF; } hp.enc_att_context_size_choices.clear(); @@ -99,10 +100,10 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, if (hp.enc_att_context_size_choices.front().first != hp.enc_att_context_left || hp.enc_att_context_size_choices.front().second != hp.enc_att_context_right) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: att_context_size_choices[0] = (%d, %d) " "but att_context_left/right = (%d, %d); " - "GGUF is inconsistent\n", + "GGUF is inconsistent", hp.enc_att_context_size_choices.front().first, hp.enc_att_context_size_choices.front().second, hp.enc_att_context_left, hp.enc_att_context_right); @@ -118,9 +119,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.enc_att_context_left, hp.enc_att_context_right); break; case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.encoder.att_context_size_choices " - "has wrong type (expected int32 array)\n"); + "has wrong type (expected int32 array)"); return TRANSCRIBE_ERR_GGUF; } } @@ -141,9 +142,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, out.clear(); return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: %s has wrong type " - "(expected int32 array)\n", key); + "(expected int32 array)", key); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_ERR_GGUF; @@ -180,19 +181,19 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.enc_att_chunk_chunk_choices.empty() || hp.enc_att_chunk_right_choices.empty()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: att_context_style=chunked_limited_with_rc " "requires non-empty att_chunk_{left,chunk,right}_choices " - "arrays in the GGUF; got %zu/%zu/%zu entries\n", + "arrays in the GGUF; got %zu/%zu/%zu entries", hp.enc_att_chunk_left_choices.size(), hp.enc_att_chunk_chunk_choices.size(), hp.enc_att_chunk_right_choices.size()); return TRANSCRIBE_ERR_GGUF; } } else { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported att_context_style \"%s\" " - "(allowed: regular, chunked_limited, chunked_limited_with_rc)\n", + "(allowed: regular, chunked_limited, chunked_limited_with_rc)", style.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -217,9 +218,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, } else if (norm_type == "layer_norm") { hp.enc_conv_norm_type = ParakeetHParams::ConvNormType::LayerNorm; } else { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported conv_norm_type \"%s\" " - "(allowed: batch_norm, layer_norm)\n", + "(allowed: batch_norm, layer_norm)", norm_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -265,9 +266,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, } else if (head_kind_str == "ctc") { hp.head_kind = HeadKind::CTC; } else { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported head_kind \"%s\" " - "(allowed: tdt, rnnt, ctc)\n", + "(allowed: tdt, rnnt, ctc)", head_kind_str.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -296,9 +297,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, break; case KvResult::Absent: case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: required KV \"stt.parakeet.tdt.durations\" " - "missing or wrong type (head_kind=tdt)\n"); + "missing or wrong type (head_kind=tdt)"); return TRANSCRIBE_ERR_GGUF; } { @@ -309,9 +310,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.tdt_max_symbols = static_cast(max_sym); break; case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: optional KV \"stt.parakeet.tdt.max_symbols\" " - "has wrong type\n"); + "has wrong type"); return TRANSCRIBE_ERR_GGUF; } } @@ -376,36 +377,36 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.prompt_dictionary_locales)) { case KvResult::Ok: break; case KvResult::Absent: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.prompt.dictionary.locales " - "is required when prompt.num_prompts > 0\n"); + "is required when prompt.num_prompts > 0"); return TRANSCRIBE_ERR_GGUF; case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.prompt.dictionary.locales " - "has wrong type (expected string array)\n"); + "has wrong type (expected string array)"); return TRANSCRIBE_ERR_GGUF; } switch (read_int32_array_kv(gguf, "stt.parakeet.prompt.dictionary.indices", hp.prompt_dictionary_indices)) { case KvResult::Ok: break; case KvResult::Absent: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.prompt.dictionary.indices " - "is required when prompt.num_prompts > 0\n"); + "is required when prompt.num_prompts > 0"); return TRANSCRIBE_ERR_GGUF; case KvResult::BadType: - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.prompt.dictionary.indices " - "has wrong type (expected int32 array)\n"); + "has wrong type (expected int32 array)"); return TRANSCRIBE_ERR_GGUF; } if (hp.prompt_dictionary_locales.size() != hp.prompt_dictionary_indices.size()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: prompt dictionary locales/indices length " - "mismatch (%zu vs %zu)\n", + "mismatch (%zu vs %zu)", hp.prompt_dictionary_locales.size(), hp.prompt_dictionary_indices.size()); return TRANSCRIBE_ERR_GGUF; @@ -423,31 +424,31 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // EncDecRNNTBPEModelWithPrompt). Other activations are a // future-variant concern. if (hp.prompt_activation != "relu") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported prompt activation \"%s\" " - "(only \"relu\" is implemented)\n", + "(only \"relu\" is implemented)", hp.prompt_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.prompt_hidden <= 0) { - std::fprintf(stderr, - "parakeet: prompt.hidden must be > 0 (got %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: prompt.hidden must be > 0 (got %d)", hp.prompt_hidden); return TRANSCRIBE_ERR_GGUF; } for (int32_t idx : hp.prompt_dictionary_indices) { if (idx < 0 || idx >= hp.prompt_num_prompts) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: prompt dictionary index %d out of range " - "[0, %d)\n", idx, hp.prompt_num_prompts); + "[0, %d)", idx, hp.prompt_num_prompts); return TRANSCRIBE_ERR_GGUF; } } if (hp.prompt_auto_id >= 0 && hp.prompt_auto_id >= hp.prompt_num_prompts) { - std::fprintf(stderr, - "parakeet: prompt.auto_id %d out of range [0, %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: prompt.auto_id %d out of range [0, %d)", hp.prompt_auto_id, hp.prompt_num_prompts); return TRANSCRIBE_ERR_GGUF; } @@ -461,22 +462,22 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.enc_d_ff <= 0 || hp.enc_conv_kernel <= 0 || hp.enc_subsampling_factor <= 0 || hp.enc_subsampling_channels <= 0) { - std::fprintf(stderr, "parakeet: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "parakeet: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.head_kind != HeadKind::CTC) { if (hp.pred_hidden <= 0 || hp.pred_n_layers <= 0 || hp.pred_vocab <= 1) { - std::fprintf(stderr, "parakeet: predictor hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: predictor hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.joint_hidden <= 0 || hp.joint_num_extra_outputs < 0) { - std::fprintf(stderr, "parakeet: joint hparams invalid\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: joint hparams invalid"); return TRANSCRIBE_ERR_GGUF; } // Joint activation allow-list. The C++ joint forward implements @@ -488,9 +489,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, hp.joint_activation != "sigmoid" && hp.joint_activation != "tanh") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported joint activation \"%s\" " - "(only relu, sigmoid, tanh are implemented)\n", + "(only relu, sigmoid, tanh are implemented)", hp.joint_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -503,61 +504,61 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // Zero is allowed and is the standard "stay on this frame, just // emit a token without advancing" duration. if (hp.tdt_durations.empty()) { - std::fprintf(stderr, - "parakeet: stt.parakeet.tdt.durations must be non-empty (head_kind=tdt)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: stt.parakeet.tdt.durations must be non-empty (head_kind=tdt)"); return TRANSCRIBE_ERR_GGUF; } if (static_cast(hp.tdt_durations.size()) != hp.joint_num_extra_outputs) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.tdt.durations length (%zu) " - "must equal joint.num_extra_outputs (%d)\n", + "must equal joint.num_extra_outputs (%d)", hp.tdt_durations.size(), hp.joint_num_extra_outputs); return TRANSCRIBE_ERR_GGUF; } for (int32_t d : hp.tdt_durations) { if (d < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.tdt.durations contains " - "negative value %d\n", d); + "negative value %d", d); return TRANSCRIBE_ERR_GGUF; } } if (hp.tdt_max_symbols < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: stt.parakeet.tdt.max_symbols must be >= 0 " - "(got %d)\n", hp.tdt_max_symbols); + "(got %d)", hp.tdt_max_symbols); return TRANSCRIBE_ERR_GGUF; } } else if (hp.head_kind == HeadKind::RNNT) { // RNNT joint emits exactly vocab+1 — no duration extras. if (hp.joint_num_extra_outputs != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: head_kind=rnnt requires joint.num_extra_outputs=0 " - "(got %d)\n", hp.joint_num_extra_outputs); + "(got %d)", hp.joint_num_extra_outputs); return TRANSCRIBE_ERR_GGUF; } } if (hp.fe_num_mels <= 0 || hp.fe_sample_rate <= 0 || hp.fe_n_fft <= 0 || hp.fe_win_length <= 0 || hp.fe_hop_length <= 0) { - std::fprintf(stderr, "parakeet: frontend dimensions must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: frontend dimensions must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_win_length > hp.fe_n_fft) { - std::fprintf(stderr, - "parakeet: frontend win_length (%d) > n_fft (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: frontend win_length (%d) > n_fft (%d)", hp.fe_win_length, hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_f_min < 0.0f || hp.fe_f_max <= hp.fe_f_min) { - std::fprintf(stderr, - "parakeet: frontend mel band invalid: f_min=%f f_max=%f\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: frontend mel band invalid: f_min=%f f_max=%f", hp.fe_f_min, hp.fe_f_max); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_dither < 0.0f) { - std::fprintf(stderr, - "parakeet: frontend dither must be >= 0 (got %f)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: frontend dither must be >= 0 (got %f)", hp.fe_dither); return TRANSCRIBE_ERR_GGUF; } @@ -565,8 +566,8 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // Recognized-but-unsupported frontend type. Surfaces as // ERR_GGUF for now; if we add a non-mel STT family later, // this turns into a NOT_IMPLEMENTED branch. - std::fprintf(stderr, - "parakeet: unsupported frontend type \"%s\" (only \"mel\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: unsupported frontend type \"%s\" (only \"mel\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -586,9 +587,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // Window: only symmetric Hann is implemented // (build_hann_window_symmetric_padded in transcribe-mel.cpp). if (hp.fe_window != "hann") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported frontend window \"%s\" " - "(only \"hann\" is implemented)\n", + "(only \"hann\" is implemented)", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -602,9 +603,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // substituting per-feature on a GGUF that asked for something else // would be a confusing bug. if (hp.fe_normalize != "per_feature" && hp.fe_normalize != "none") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: unsupported frontend normalize \"%s\" " - "(only \"per_feature\" and \"none\" are implemented)\n", + "(only \"per_feature\" and \"none\" are implemented)", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -616,9 +617,9 @@ transcribe_status read_parakeet_hparams(const gguf_context * gguf, // n_fft=400 fails at load instead of producing nonsense mels. // (fe_n_fft > 0 was already enforced above.) if ((hp.fe_n_fft & (hp.fe_n_fft - 1)) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: frontend n_fft (%d) must be a power of 2 " - "(radix-2 FFT requirement)\n", + "(radix-2 FFT requirement)", hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } @@ -864,13 +865,13 @@ transcribe_status build_parakeet_weights(ggml_context * ctx_meta, // specified expected shape so the type+rank checks still fire. ggml_tensor * ctc_peek = ggml_get_tensor(ctx_meta, "head.ctc.weight"); if (ctc_peek == nullptr) { - std::fprintf(stderr, "parakeet: missing tensor \"head.ctc.weight\"\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet: missing tensor \"head.ctc.weight\""); return TRANSCRIBE_ERR_GGUF; } const int64_t n_classes = ctc_peek->ne[2]; if (n_classes <= 1) { - std::fprintf(stderr, - "parakeet: head.ctc.weight vocab+1 (=%lld) must be > 1\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "parakeet: head.ctc.weight vocab+1 (=%lld) must be > 1", (long long)n_classes); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/qwen3_asr/decoder.cpp b/src/arch/qwen3_asr/decoder.cpp index ee6f9a90..cd13ade3 100644 --- a/src/arch/qwen3_asr/decoder.cpp +++ b/src/arch/qwen3_asr/decoder.cpp @@ -21,6 +21,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -85,27 +86,27 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, pb.suffix_len = suffix_len; if (ctx == nullptr || T_prompt <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "qwen3_asr decoder: invalid arg (T_prompt=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr decoder: invalid arg (T_prompt=%d, T_enc=%d)", T_prompt, T_enc); return pb; } if (prefix_len < 0 || suffix_len < 0 || prefix_len + T_enc + suffix_len != T_prompt) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr decoder: prefix_len(%d) + T_enc(%d) + " - "suffix_len(%d) != T_prompt(%d)\n", + "suffix_len(%d) != T_prompt(%d)", prefix_len, T_enc, suffix_len, T_prompt); return pb; } if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { - std::fprintf(stderr, "qwen3_asr decoder: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr decoder: kv_cache not initialized"); return pb; } if (T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, - "qwen3_asr decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d", T_prompt, kv_cache.n_ctx); return pb; } @@ -141,8 +142,8 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "qwen3_asr decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr decoder: ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -273,17 +274,17 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0) { - std::fprintf(stderr, - "qwen3_asr step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, "qwen3_asr step: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr step: kv_cache not initialized"); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "qwen3_asr step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -316,8 +317,8 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "qwen3_asr step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step: ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -380,25 +381,25 @@ PrefillBuildBatched build_prefill_graph_batched( pb.n_batch = n_batch; if (ctx == nullptr || T_prompt_max <= 0 || T_enc_max <= 0 || n_batch <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr prefill(batched): invalid arg " - "(T_prompt_max=%d, T_enc_max=%d, n_batch=%d)\n", + "(T_prompt_max=%d, T_enc_max=%d, n_batch=%d)", T_prompt_max, T_enc_max, n_batch); return pb; } if (kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch) { - std::fprintf(stderr, - "qwen3_asr prefill(batched): kv_cache mismatch\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr prefill(batched): kv_cache mismatch"); return pb; } if (!use_flash) { - std::fprintf(stderr, - "qwen3_asr prefill(batched): requires use_flash\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr prefill(batched): requires use_flash"); return pb; } if (T_prompt_max > kv_cache.n_ctx) { - std::fprintf(stderr, - "qwen3_asr prefill(batched): T_prompt_max=%d > n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr prefill(batched): T_prompt_max=%d > n_ctx=%d", T_prompt_max, kv_cache.n_ctx); return pb; } @@ -447,8 +448,8 @@ PrefillBuildBatched build_prefill_graph_batched( ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "qwen3_asr prefill(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr prefill(batched): ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -475,8 +476,8 @@ PrefillBuildBatched build_prefill_graph_batched( T_prompt_max, B, pb.mask_in, pb.positions_in, pb.kv_idx_in, use_flash); if (x == nullptr) { - std::fprintf(stderr, - "qwen3_asr prefill(batched): block %d failed\n", il); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr prefill(batched): block %d failed", il); pb.graph = nullptr; return pb; } @@ -517,31 +518,31 @@ StepBuildBatched build_step_graph_batched( sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr step(batched): invalid arg " - "(max_n_kv=%d, n_batch=%d)\n", max_n_kv, n_batch); + "(max_n_kv=%d, n_batch=%d)", max_n_kv, n_batch); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, - "qwen3_asr step(batched): kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): kv_cache not initialized"); return sb; } if (kv_cache.n_batch != n_batch) { - std::fprintf(stderr, - "qwen3_asr step(batched): kv_cache.n_batch=%d != %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): kv_cache.n_batch=%d != %d", kv_cache.n_batch, n_batch); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "qwen3_asr step(batched): max_n_kv=%d exceeds n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): max_n_kv=%d exceeds n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } if (!use_flash) { - std::fprintf(stderr, - "qwen3_asr step(batched): requires use_flash\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): requires use_flash"); return sb; } @@ -571,8 +572,8 @@ StepBuildBatched build_step_graph_batched( ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, - "qwen3_asr step(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -596,8 +597,8 @@ StepBuildBatched build_step_graph_batched( sb.kv_idx_in, use_flash); if (x == nullptr) { - std::fprintf(stderr, - "qwen3_asr step(batched): block %d build failed\n", il); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step(batched): block %d build failed", il); sb.graph = nullptr; return sb; } diff --git a/src/arch/qwen3_asr/encoder.cpp b/src/arch/qwen3_asr/encoder.cpp index 96bdc0bd..d3657658 100644 --- a/src/arch/qwen3_asr/encoder.cpp +++ b/src/arch/qwen3_asr/encoder.cpp @@ -6,6 +6,7 @@ #include "encoder.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -215,9 +216,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.timing = timing; if (ctx == nullptr || timing.n_chunks <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr encoder: invalid arg " - "(ctx=%p, n_chunks=%d)\n", + "(ctx=%p, n_chunks=%d)", static_cast(ctx), timing.n_chunks); return eb; } @@ -294,9 +295,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int64_t W_ds = x->ne[0]; const int64_t H_ds = x->ne[1]; if (W_ds != T_per_chunk) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr encoder: post-conv W=%lld does not match " - "per_chunk_aftercnn=%lld\n", + "per_chunk_aftercnn=%lld", static_cast(W_ds), static_cast(T_per_chunk)); return eb; @@ -406,8 +407,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // leaves headroom for debug-name nodes. eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "qwen3_asr encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -433,9 +434,9 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, { EncoderBuildBatched eb {}; if (ctx == nullptr || n_chunks_max <= 0 || n_batch <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr encoder(batched): invalid arg " - "(n_chunks_max=%d, n_batch=%d)\n", n_chunks_max, n_batch); + "(n_chunks_max=%d, n_batch=%d)", n_chunks_max, n_batch); return eb; } @@ -487,9 +488,9 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, const int64_t W_ds = x->ne[0]; const int64_t H_ds = x->ne[1]; if (W_ds != T_per_chunk) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr encoder(batched): post-conv W=%lld != " - "per_chunk_aftercnn=%lld\n", + "per_chunk_aftercnn=%lld", static_cast(W_ds), static_cast(T_per_chunk)); return eb; @@ -531,8 +532,8 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "qwen3_asr encoder(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr encoder(batched): ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/qwen3_asr/model.cpp b/src/arch/qwen3_asr/model.cpp index be534b2f..217d75af 100644 --- a/src/arch/qwen3_asr/model.cpp +++ b/src/arch/qwen3_asr/model.cpp @@ -208,14 +208,14 @@ transcribe_status load( m->hparams.eos_token_id = m->tok.eos_id(); if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "qwen3_asr: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, - "qwen3_asr: GGUF tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: GGUF tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } @@ -299,8 +299,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "qwen3_asr: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -417,8 +417,8 @@ transcribe_status resolve_chat_tokens(const transcribe::Tokenizer & tok, for (const auto & p : pieces) { const int id = tok.find(p.piece); if (id < 0) { - std::fprintf(stderr, - "qwen3_asr: chat-template piece \"%s\" not in tokenizer\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: chat-template piece \"%s\" not in tokenizer", p.piece); return TRANSCRIBE_ERR_GGUF; } @@ -570,22 +570,22 @@ transcribe_status encode_language_prefix(const transcribe::Tokenizer & tok, } } if (pub_name == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: no canonical publisher name for " - "language=\"%s\"\n", bcp47); + "language=\"%s\"", bcp47); return TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE; } if (!tok.has_encoder()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: tokenizer missing encoder (merges " - "unavailable); cannot render language hint\n"); + "unavailable); cannot render language hint"); return TRANSCRIBE_ERR_GGUF; } const int asr_text_id = tok.find(""); if (asr_text_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: tokenizer vocab missing " - "special token\n"); + "special token"); return TRANSCRIBE_ERR_GGUF; } std::string text = "language "; @@ -683,8 +683,8 @@ transcribe_status run( // ----- Mel front-end ------------------------------------------- if (!cm->mel.has_value()) { - std::fprintf(stderr, - "qwen3_asr run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } const int64_t t_mel_start = ggml_time_us(); @@ -696,8 +696,8 @@ transcribe_status run( cc->n_threads); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "qwen3_asr run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } @@ -717,9 +717,9 @@ transcribe_status run( // ----- Compute encoder timing + reject unsupported shapes ------ EncoderTiming timing = compute_encoder_timing(mel_n_frames, cm->hparams); if (timing.n_chunks <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr run: encoder timing is degenerate " - "(n_mel_frames=%d)\n", mel_n_frames); + "(n_mel_frames=%d)", mel_n_frames); return TRANSCRIBE_ERR_GGUF; } @@ -751,8 +751,8 @@ transcribe_status run( ip.no_alloc = true; cc->compute_ctx = ggml_init(ip); if (cc->compute_ctx == nullptr) { - std::fprintf(stderr, - "qwen3_asr run: ggml_init for compute_ctx failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: ggml_init for compute_ctx failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -772,8 +772,8 @@ transcribe_status run( static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "qwen3_asr run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -838,8 +838,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "qwen3_asr run: encoder graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: encoder graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1013,8 +1013,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "qwen3_asr run: prefill graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr run: prefill graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1152,8 +1152,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, sb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "qwen3_asr step: graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr step: graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1264,7 +1264,7 @@ transcribe_status run( t_prefill_logits_us + t_step_loop_us) * ms; const double per_step_ms = (n_steps > 0) ? (t_step_loop_us * ms / n_steps) : 0.0; - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "qwen3_asr perf breakdown:\n" " mel %8.2f ms\n" " enc_build %8.2f ms (graph + sched + uploads)\n" @@ -1281,7 +1281,7 @@ transcribe_status run( " compute %8.2f ms (%.3f ms/step)\n" " tensor_get %8.2f ms (%.3f ms/step)\n" " ---\n" - " sum %8.2f ms\n", + " sum %8.2f ms", cc->t_mel_us * ms, t_enc_build_us * ms, cc->t_encode_us * ms, @@ -1930,11 +1930,11 @@ transcribe_status run_batch( if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e != nullptr && *e != '\0' && *e != '0') { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "qwen3_asr run_batch: n=%d valid=%d max_n_kv=%d steps=%d (all phases batched x%d)\n" " enc_pass=%.1fms (mel=%.1f parallel + enc_compute=%.1f, 1 graph)\n" " prefill_pass=%.1fms (1 batched graph: build/sched/compute/readback)\n" - " step_loop=%.1fms (%.2fms/step)\n", + " step_loop=%.1fms (%.2fms/step)", n, valid_count, max_n_kv, n_steps, valid_count, enc_pass_us / 1000.0, mel_us / 1000.0, enc_us / 1000.0, prefill_pass_us / 1000.0, diff --git a/src/arch/qwen3_asr/weights.cpp b/src/arch/qwen3_asr/weights.cpp index 0acc6f40..24f1fdc3 100644 --- a/src/arch/qwen3_asr/weights.cpp +++ b/src/arch/qwen3_asr/weights.cpp @@ -14,6 +14,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -99,8 +100,8 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, case KvResult::Ok: dst = static_cast(tmp); return TRANSCRIBE_OK; case KvResult::Absent: return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, - "qwen3_asr: \"%s\" has wrong type\n", key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: \"%s\" has wrong type", key); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_ERR_GGUF; // unreachable @@ -115,41 +116,41 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, hp.enc_ffn_dim <= 0 || hp.enc_downsample_hidden <= 0 || hp.enc_output_dim <= 0 || hp.enc_num_mel_bins <= 0) { - std::fprintf(stderr, "qwen3_asr: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "qwen3_asr: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { - std::fprintf(stderr, "qwen3_asr: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, - "qwen3_asr: n_heads (%d) not divisible by n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden_act != "silu" && hp.dec_hidden_act != "swish") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: unsupported decoder hidden_act \"%s\" " - "(only silu/swish)\n", hp.dec_hidden_act.c_str()); + "(only silu/swish)", hp.dec_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_activation != "gelu") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: unsupported encoder activation \"%s\" " - "(only gelu)\n", hp.enc_activation.c_str()); + "(only gelu)", hp.enc_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, "qwen3_asr: unsupported frontend type \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: unsupported frontend type \"%s\"", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -158,9 +159,9 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, // untied lm_head tensor that this port does not ship or consume; // fail at load rather than silently run an undefined graph. if (!hp.dec_tie_word_embeddings) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: decoder.tie_word_embeddings=false is not " - "supported in this port (graph assumes tied lm_head)\n"); + "supported in this port (graph assumes tied lm_head)"); return TRANSCRIBE_ERR_GGUF; } // Single-modality audio-LLM: the LM at inference processes one @@ -176,9 +177,9 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, hp.dec_rope_mrope_section_w; const int32_t half = hp.dec_head_dim / 2; if (section_sum != half) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: mrope_section t+h+w (%d) != head_dim/2 (%d); " - "single-modality RoPE reduction no longer valid\n", + "single-modality RoPE reduction no longer valid", section_sum, half); return TRANSCRIBE_ERR_GGUF; } @@ -188,10 +189,10 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, // if a future checkpoint used the concatenated layout, so fail // at load rather than silently produce wrong logits. if (!hp.dec_rope_mrope_interleaved) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: dec_rope_mrope_interleaved=false is not " "supported; the decoder graph assumes the interleaved " - "MRoPE layout\n"); + "MRoPE layout"); return TRANSCRIBE_ERR_GGUF; } } @@ -201,9 +202,9 @@ transcribe_status read_qwen3_asr_hparams(const gguf_context * gguf, // surface at graph-build time anyway; catching it at load gives // a clearer error and avoids paying encoder cost on every run(). if (hp.enc_output_dim != hp.dec_hidden) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: enc_output_dim (%d) != dec_hidden (%d); " - "audio injection requires matching dims\n", + "audio injection requires matching dims", hp.enc_output_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } @@ -320,20 +321,20 @@ transcribe_status build_qwen3_asr_weights(ggml_context * ctx_meta, { ggml_tensor * tw = ggml_get_tensor(ctx_meta, "dec.token_embd.weight"); if (tw == nullptr) { - std::fprintf(stderr, - "qwen3_asr: missing tensor \"dec.token_embd.weight\"\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: missing tensor \"dec.token_embd.weight\""); return TRANSCRIBE_ERR_GGUF; } if (tw->ne[0] != hp.dec_hidden) { - std::fprintf(stderr, - "qwen3_asr: dec.token_embd.weight ne[0]=%lld, expected %lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: dec.token_embd.weight ne[0]=%lld, expected %lld", static_cast(tw->ne[0]), static_cast(hp.dec_hidden)); return TRANSCRIBE_ERR_GGUF; } if (tw->ne[1] != hp.dec_vocab_size) { - std::fprintf(stderr, - "qwen3_asr: dec.token_embd.weight ne[1]=%lld, expected %lld\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "qwen3_asr: dec.token_embd.weight ne[1]=%lld, expected %lld", static_cast(tw->ne[1]), static_cast(hp.dec_vocab_size)); return TRANSCRIBE_ERR_GGUF; @@ -366,9 +367,9 @@ transcribe_status build_qwen3_asr_weights(ggml_context * ctx_meta, GET_LIN(b.ffn_down_w, lname("dec.blocks.%d.ffn.down.weight", i), dec_im, dec_h); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "qwen3_asr: ffn gate/up dtype mismatch at layer %d " - "(%d vs %d)\n", i, + "(%d vs %d)", i, static_cast(b.ffn_gate_w->type), static_cast(b.ffn_up_w->type)); return TRANSCRIBE_ERR_GGUF; diff --git a/src/arch/sensevoice/encoder.cpp b/src/arch/sensevoice/encoder.cpp index 15636f33..b7962450 100644 --- a/src/arch/sensevoice/encoder.cpp +++ b/src/arch/sensevoice/encoder.cpp @@ -35,6 +35,7 @@ #include "conformer/conformer.h" #include "sanm/sanm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -89,9 +90,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_lfr_frames <= 0 || n_batch <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice encoder: invalid arg " - "(ctx=%p, n_lfr_frames=%d, n_batch=%d)\n", + "(ctx=%p, n_lfr_frames=%d, n_batch=%d)", static_cast(ctx), n_lfr_frames, n_batch); return eb; } @@ -268,8 +269,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // 8192 leaves ample headroom. eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "sensevoice encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/sensevoice/model.cpp b/src/arch/sensevoice/model.cpp index 77816239..96e5b2a3 100644 --- a/src/arch/sensevoice/model.cpp +++ b/src/arch/sensevoice/model.cpp @@ -164,8 +164,8 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "sensevoice: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -458,9 +458,9 @@ transcribe_status run( cc->t_mel_us = ggml_time_us() - t_mel_start; if (T_lfr <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice run: input too short for kaldi fbank " - "(n_samples=%d → T_lfr=0)\n", n_samples); + "(n_samples=%d → T_lfr=0)", n_samples); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -551,7 +551,7 @@ transcribe_status run( // Sinusoidal PE: depth = current width = d_input (NOT d_model). transcribe::sanm::build_sinusoidal_pe(cc->pe_buf, hp.enc_d_input, T_full); if (eb.pe_in == nullptr) { - std::fprintf(stderr, "sensevoice run: pe.in not found in graph\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice run: pe.in not found in graph"); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_set(eb.pe_in, cc->pe_buf.data(), @@ -569,8 +569,8 @@ transcribe_status run( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "sensevoice run: graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice run: graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -802,8 +802,8 @@ static transcribe_status run_batch_encode( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "sensevoice run_batch: graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice run_batch: graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/sensevoice/weights.cpp b/src/arch/sensevoice/weights.cpp index 4fc37a8e..a1123570 100644 --- a/src/arch/sensevoice/weights.cpp +++ b/src/arch/sensevoice/weights.cpp @@ -12,6 +12,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -83,17 +84,17 @@ transcribe_status read_sensevoice_hparams(const gguf_context * gguf, hp.enc_n_heads <= 0 || hp.enc_d_ff <= 0 || hp.enc_kernel <= 0 || hp.enc_kernel % 2 == 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: encoder hparams invalid (n_blocks=%d " "tp_blocks=%d d_model=%d d_input=%d n_heads=%d d_ff=%d " - "kernel=%d — kernel must be positive and odd)\n", + "kernel=%d — kernel must be positive and odd)", hp.enc_n_blocks, hp.enc_tp_blocks, hp.enc_d_model, hp.enc_d_input, hp.enc_n_heads, hp.enc_d_ff, hp.enc_kernel); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "sensevoice: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } @@ -102,50 +103,50 @@ transcribe_status read_sensevoice_hparams(const gguf_context * gguf, hp.fe_hop_length <= 0 || hp.fe_lfr_m <= 0 || hp.fe_lfr_n <= 0) { - std::fprintf(stderr, - "sensevoice: frontend dimensions must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "sensevoice: frontend dimensions must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_input != hp.fe_num_mels * hp.fe_lfr_m) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: encoder d_input (%d) must equal num_mels (%d) " - "× lfr_m (%d) = %d\n", + "× lfr_m (%d) = %d", hp.enc_d_input, hp.fe_num_mels, hp.fe_lfr_m, hp.fe_num_mels * hp.fe_lfr_m); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_attn_type != "sanm") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: unsupported attention type \"%s\" " - "(only \"sanm\" is implemented)\n", + "(only \"sanm\" is implemented)", hp.enc_attn_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "kaldi_fbank_lfr") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: unsupported frontend type \"%s\" " - "(only \"kaldi_fbank_lfr\" is implemented)\n", + "(only \"kaldi_fbank_lfr\" is implemented)", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_window != "hamming") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: unsupported frontend window \"%s\" " - "(only \"hamming\" is implemented)\n", + "(only \"hamming\" is implemented)", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_normalize != "per_feature") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: unsupported frontend normalize \"%s\" " - "(only \"per_feature\" is implemented)\n", + "(only \"per_feature\" is implemented)", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_fbank_style != "kaldi_htk") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: unsupported fbank_style \"%s\" " - "(only \"kaldi_htk\" is implemented)\n", + "(only \"kaldi_htk\" is implemented)", hp.fe_fbank_style.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -263,9 +264,9 @@ transcribe_status build_sensevoice_weights(ggml_context * ctx_meta, const int64_t vocab = hp.vocab_size; if (vocab <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "sensevoice: build_sensevoice_weights called before " - "tokenizer load (vocab_size=%d)\n", hp.vocab_size); + "tokenizer load (vocab_size=%d)", hp.vocab_size); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/voxtral/decoder.cpp b/src/arch/voxtral/decoder.cpp index 0bd115da..2193eb8d 100644 --- a/src/arch/voxtral/decoder.cpp +++ b/src/arch/voxtral/decoder.cpp @@ -11,6 +11,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -74,26 +75,26 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, pb.suffix_len = suffix_len; if (ctx == nullptr || T_prompt <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "voxtral decoder: invalid arg (T_prompt=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral decoder: invalid arg (T_prompt=%d, T_enc=%d)", T_prompt, T_enc); return pb; } if (prefix_len < 0 || suffix_len < 0 || prefix_len + T_enc + suffix_len != T_prompt) { - std::fprintf(stderr, - "voxtral decoder: prefix(%d)+T_enc(%d)+suffix(%d) != T_prompt(%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral decoder: prefix(%d)+T_enc(%d)+suffix(%d) != T_prompt(%d)", prefix_len, T_enc, suffix_len, T_prompt); return pb; } if (kv_cache.self_k == nullptr || kv_cache.self_v == nullptr) { - std::fprintf(stderr, "voxtral decoder: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral decoder: kv_cache not initialized"); return pb; } if (T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, - "voxtral decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral decoder: T_prompt=%d exceeds kv_cache.n_ctx=%d", T_prompt, kv_cache.n_ctx); return pb; } @@ -124,7 +125,7 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "voxtral decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral decoder: ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -238,16 +239,16 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0) { - std::fprintf(stderr, "voxtral step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } if (kv_cache.self_k == nullptr) { - std::fprintf(stderr, "voxtral step: kv_cache not initialized\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral step: kv_cache not initialized"); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "voxtral step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -276,7 +277,7 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "voxtral step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral step: ggml_new_graph_custom failed"); return sb; } sb.graph = gf; @@ -337,7 +338,7 @@ PrefillBuildBatched build_prefill_graph_batched( n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt_max > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral prefill(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral prefill(batched): invalid arg"); return pb; } @@ -372,7 +373,7 @@ PrefillBuildBatched build_prefill_graph_batched( ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "voxtral prefill(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral prefill(batched): ggml_new_graph_custom failed"); return pb; } pb.graph = gf; @@ -427,7 +428,7 @@ StepBuildBatched build_step_graph_batched( if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral step(batched): invalid arg\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral step(batched): invalid arg"); return sb; } @@ -448,7 +449,7 @@ StepBuildBatched build_step_graph_batched( ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "voxtral step(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral step(batched): ggml_new_graph_custom failed"); return sb; } sb.graph = gf; diff --git a/src/arch/voxtral/encoder.cpp b/src/arch/voxtral/encoder.cpp index fa40ab4e..502816d6 100644 --- a/src/arch/voxtral/encoder.cpp +++ b/src/arch/voxtral/encoder.cpp @@ -25,6 +25,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -219,8 +220,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0 || n_mel_frames % 2 != 0) { - std::fprintf(stderr, - "voxtral encoder: invalid n_mel_frames=%d (must be positive even)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral encoder: invalid n_mel_frames=%d (must be positive even)", n_mel_frames); return eb; } @@ -231,9 +232,9 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int T_enc = n_mel_frames / 2; if (T_enc != hp.enc_max_source_positions) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral encoder: T_enc=%d != max_source_positions=%d " - "(mel must be padded to %d frames)\n", + "(mel must be padded to %d frames)", T_enc, hp.enc_max_source_positions, 2 * hp.enc_max_source_positions); return eb; @@ -296,7 +297,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "voxtral encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -320,9 +321,9 @@ EncoderBuild build_encoder_graph_batched(ggml_context * ctx, if (ctx == nullptr || n_mel_frames <= 0 || n_mel_frames % 2 != 0 || n_chunks <= 0 || !use_flash) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral encoder(batched): invalid arg " - "(n_mel_frames=%d, n_chunks=%d, use_flash=%d)\n", + "(n_mel_frames=%d, n_chunks=%d, use_flash=%d)", n_mel_frames, n_chunks, static_cast(use_flash)); return eb; } @@ -334,8 +335,8 @@ EncoderBuild build_encoder_graph_batched(ggml_context * ctx, const int B = n_chunks; if (T_enc != hp.enc_max_source_positions) { - std::fprintf(stderr, - "voxtral encoder(batched): T_enc=%d != max_source_positions=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral encoder(batched): T_enc=%d != max_source_positions=%d", T_enc, hp.enc_max_source_positions); return eb; } @@ -382,7 +383,7 @@ EncoderBuild build_encoder_graph_batched(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "voxtral encoder(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral encoder(batched): ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/voxtral/model.cpp b/src/arch/voxtral/model.cpp index 3a901897..912f17d9 100644 --- a/src/arch/voxtral/model.cpp +++ b/src/arch/voxtral/model.cpp @@ -216,8 +216,8 @@ transcribe_status resolve_specials(const transcribe::Tokenizer & tok, for (const auto & p : pieces) { const int id = tok.find(p.piece); if (id < 0) { - std::fprintf(stderr, - "voxtral: control token \"%s\" not in tokenizer\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: control token \"%s\" not in tokenizer", p.piece); return TRANSCRIBE_ERR_GGUF; } @@ -226,7 +226,7 @@ transcribe_status resolve_specials(const transcribe::Tokenizer & tok, out.bos = tok.bos_id(); out.eos = tok.eos_id(); if (out.bos < 0 || out.eos < 0) { - std::fprintf(stderr, "voxtral: tokenizer missing bos/eos id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: tokenizer missing bos/eos id"); return TRANSCRIBE_ERR_GGUF; } (void)hp; @@ -255,7 +255,7 @@ transcribe_status build_transcription_prompt(const VoxtralModel & m, if (const transcribe_status st = m.tok.encode(lang_str, lang_ids); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "voxtral: failed to encode \"%s\"\n", lang_str.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: failed to encode \"%s\"", lang_str.c_str()); return st; } out_ids.insert(out_ids.end(), lang_ids.begin(), lang_ids.end()); @@ -284,7 +284,7 @@ transcribe_status build_instruct_prompt(const VoxtralModel & m, if (const transcribe_status st = m.tok.encode(instruction, instr_ids); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "voxtral: failed to encode instruction text\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: failed to encode instruction text"); return st; } out_ids.insert(out_ids.end(), instr_ids.begin(), instr_ids.end()); @@ -361,13 +361,13 @@ transcribe_status load( m->hparams.eos_token_id = m->tok.eos_id(); if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, - "voxtral: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } if (m->hparams.eos_token_id < 0) { - std::fprintf(stderr, "voxtral: GGUF tokenizer has no eos_token_id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: GGUF tokenizer has no eos_token_id"); return TRANSCRIBE_ERR_GGUF; } @@ -434,7 +434,7 @@ transcribe_status load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, "voxtral: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -564,7 +564,7 @@ transcribe_status run( } if (!cm->mel.has_value()) { - std::fprintf(stderr, "voxtral run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -593,19 +593,19 @@ transcribe_status run( cc->n_threads); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, "voxtral run: MelFrontend::compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: MelFrontend::compute failed (%s)", transcribe_status_string(mst)); return mst; } cc->t_mel_us = ggml_time_us() - t_mel_start; if (mel_n_mels != cm->hparams.enc_num_mel_bins) { - std::fprintf(stderr, "voxtral run: mel bins %d != %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: mel bins %d != %d", mel_n_mels, cm->hparams.enc_num_mel_bins); return TRANSCRIBE_ERR_GGUF; } if (mel_n_frames < n_chunks * frames_per_chunk) { - std::fprintf(stderr, "voxtral run: mel frames %d < %d*%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: mel frames %d < %d*%d", mel_n_frames, n_chunks, frames_per_chunk); return TRANSCRIBE_ERR_GGUF; } @@ -628,7 +628,7 @@ transcribe_status run( cm->plan.scheduler_list.data(), nullptr, static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, "voxtral run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -676,7 +676,7 @@ transcribe_status run( if (const ggml_status gs = ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral run: encoder compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: encoder compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -814,7 +814,7 @@ transcribe_status run( if (const ggml_status gs = ggml_backend_sched_graph_compute(cc->sched, pb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral run: prefill compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: prefill compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -912,7 +912,7 @@ transcribe_status run( if (const ggml_status gs = ggml_backend_sched_graph_compute(cc->sched, sb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral run: step compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral run: step compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/voxtral/weights.cpp b/src/arch/voxtral/weights.cpp index a765d736..0f720ac9 100644 --- a/src/arch/voxtral/weights.cpp +++ b/src/arch/voxtral/weights.cpp @@ -12,6 +12,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -86,7 +87,7 @@ transcribe_status read_voxtral_hparams(const gguf_context * gguf, case KvResult::Ok: dst = static_cast(tmp); return TRANSCRIBE_OK; case KvResult::Absent: return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, "voxtral: \"%s\" has wrong type\n", key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: \"%s\" has wrong type", key); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_ERR_GGUF; @@ -101,69 +102,69 @@ transcribe_status read_voxtral_hparams(const gguf_context * gguf, hp.enc_ffn_dim <= 0 || hp.enc_num_mel_bins <= 0 || hp.enc_max_source_positions <= 0) { - std::fprintf(stderr, "voxtral: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "voxtral: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_head_dim != hp.enc_d_model / hp.enc_n_heads) { - std::fprintf(stderr, - "voxtral: encoder head_dim (%d) != d_model/n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: encoder head_dim (%d) != d_model/n_heads (%d)", hp.enc_head_dim, hp.enc_d_model / hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.proj_downsample <= 0 || hp.proj_in <= 0) { - std::fprintf(stderr, "voxtral: projector hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: projector hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.proj_in != hp.enc_d_model * hp.proj_downsample) { - std::fprintf(stderr, - "voxtral: projector input_dim (%d) != enc_d_model*downsample (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: projector input_dim (%d) != enc_d_model*downsample (%d)", hp.proj_in, hp.enc_d_model * hp.proj_downsample); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_max_source_positions % hp.proj_downsample != 0) { - std::fprintf(stderr, - "voxtral: max_source_positions (%d) not divisible by downsample (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: max_source_positions (%d) not divisible by downsample (%d)", hp.enc_max_source_positions, hp.proj_downsample); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { - std::fprintf(stderr, "voxtral: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, - "voxtral: n_heads (%d) not divisible by n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden_act != "silu" && hp.dec_hidden_act != "swish") { - std::fprintf(stderr, - "voxtral: unsupported decoder hidden_act \"%s\" (only silu/swish)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: unsupported decoder hidden_act \"%s\" (only silu/swish)", hp.dec_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_activation != "gelu") { - std::fprintf(stderr, - "voxtral: unsupported encoder activation \"%s\" (only gelu)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: unsupported encoder activation \"%s\" (only gelu)", hp.enc_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.proj_hidden_act != "gelu") { - std::fprintf(stderr, - "voxtral: unsupported projector hidden_act \"%s\" (only gelu)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: unsupported projector hidden_act \"%s\" (only gelu)", hp.proj_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, "voxtral: unsupported frontend type \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral: unsupported frontend type \"%s\"", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -284,8 +285,8 @@ transcribe_status build_voxtral_weights(ggml_context * ctx_meta, GET_LIN(b.ffn_down_w, lname("dec.blocks.%d.ffn.down.weight", i), dec_im, dec_h); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, - "voxtral: ffn gate/up dtype mismatch at layer %d (%d vs %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "voxtral: ffn gate/up dtype mismatch at layer %d (%d vs %d)", i, static_cast(b.ffn_gate_w->type), static_cast(b.ffn_up_w->type)); return TRANSCRIBE_ERR_GGUF; diff --git a/src/arch/voxtral_realtime/decoder.cpp b/src/arch/voxtral_realtime/decoder.cpp index 15afb5ed..d68fc7f1 100644 --- a/src/arch/voxtral_realtime/decoder.cpp +++ b/src/arch/voxtral_realtime/decoder.cpp @@ -9,6 +9,7 @@ #include "causal_lm/causal_lm.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -75,7 +76,7 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, PrefillBuild pb {}; pb.T = T; if (ctx == nullptr || T <= 0 || kv_cache.self_k == nullptr || T > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral_realtime prefill: invalid arg (T=%d)\n", T); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime prefill: invalid arg (T=%d)", T); return pb; } @@ -102,7 +103,7 @@ PrefillBuild build_prefill_graph(ggml_context * ctx, ggml_set_input(pb.mask_in); ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); - if (gf == nullptr) { std::fprintf(stderr, "voxtral_realtime prefill: graph alloc failed\n"); return pb; } + if (gf == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime prefill: graph alloc failed"); return pb; } pb.graph = gf; // Token embedding + additive audio overlay. @@ -181,7 +182,7 @@ StepBuild build_step_graph(ggml_context * ctx, StepBuild sb {}; sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0 || kv_cache.self_k == nullptr || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral_realtime step: invalid arg (max_n_kv=%d)\n", max_n_kv); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime step: invalid arg (max_n_kv=%d)", max_n_kv); return sb; } @@ -208,7 +209,7 @@ StepBuild build_step_graph(ggml_context * ctx, ggml_set_input(sb.mask_in); ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); - if (gf == nullptr) { std::fprintf(stderr, "voxtral_realtime step: graph alloc failed\n"); return sb; } + if (gf == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime step: graph alloc failed"); return sb; } sb.graph = gf; ggml_tensor * x = ggml_get_rows(ctx, weights.dec_embed.token_w, sb.input_id_in); @@ -250,7 +251,7 @@ VerifyBuild build_verify_graph(ggml_context * ctx, vb.max_n_kv = max_n_kv; if (ctx == nullptr || T_verify <= 0 || max_n_kv <= 0 || kv_cache.self_k == nullptr || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral_realtime verify: invalid arg (T_verify=%d max_n_kv=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime verify: invalid arg (T_verify=%d max_n_kv=%d)", T_verify, max_n_kv); return vb; } @@ -279,7 +280,7 @@ VerifyBuild build_verify_graph(ggml_context * ctx, ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (gf == nullptr) { - std::fprintf(stderr, "voxtral_realtime verify: graph alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime verify: graph alloc failed"); return vb; } vb.graph = gf; @@ -329,7 +330,7 @@ PrefillBuildBatched build_prefill_graph_batched( if (ctx == nullptr || T_prompt <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || T_prompt > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral_realtime prefill(batched): invalid arg (T_prompt=%d, n_batch=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime prefill(batched): invalid arg (T_prompt=%d, n_batch=%d)", T_prompt, n_batch); return pb; } @@ -362,7 +363,7 @@ PrefillBuildBatched build_prefill_graph_batched( ggml_set_input(pb.last_idx_in); ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/16384, /*grads=*/false); - if (gf == nullptr) { std::fprintf(stderr, "voxtral_realtime prefill(batched): graph alloc failed\n"); return pb; } + if (gf == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime prefill(batched): graph alloc failed"); return pb; } pb.graph = gf; // Token-embed the (rectangular) prompt rows, then ADD the audio embeds at @@ -413,7 +414,7 @@ StepBuildBatched build_step_graph_batched( if (ctx == nullptr || max_n_kv <= 0 || n_batch <= 0 || !use_flash || kv_cache.self_k == nullptr || kv_cache.n_batch != n_batch || max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, "voxtral_realtime step(batched): invalid arg (max_n_kv=%d, n_batch=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime step(batched): invalid arg (max_n_kv=%d, n_batch=%d)", max_n_kv, n_batch); return sb; } @@ -442,7 +443,7 @@ StepBuildBatched build_step_graph_batched( ggml_set_input(sb.mask_in); ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); - if (gf == nullptr) { std::fprintf(stderr, "voxtral_realtime step(batched): graph alloc failed\n"); return sb; } + if (gf == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime step(batched): graph alloc failed"); return sb; } sb.graph = gf; ggml_tensor * x = ggml_get_rows(ctx, weights.dec_embed.token_w, sb.input_ids_in); // [hidden, B] diff --git a/src/arch/voxtral_realtime/encoder.cpp b/src/arch/voxtral_realtime/encoder.cpp index a6b1be1a..13da067c 100644 --- a/src/arch/voxtral_realtime/encoder.cpp +++ b/src/arch/voxtral_realtime/encoder.cpp @@ -22,6 +22,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -249,7 +250,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 4) { - std::fprintf(stderr, "voxtral_realtime encoder: invalid n_mel_frames=%d\n", n_mel_frames); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder: invalid n_mel_frames=%d", n_mel_frames); return eb; } @@ -262,7 +263,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int T_enc = (n_mel_frames - 2) / 2 + 1; const int n_audio = T_enc / hp.proj_downsample; if (n_audio <= 0) { - std::fprintf(stderr, "voxtral_realtime encoder: T_enc=%d too small\n", T_enc); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder: T_enc=%d too small", T_enc); return eb; } eb.T_enc = T_enc; @@ -344,7 +345,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "voxtral_realtime encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -364,9 +365,9 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, EncoderBuildBatched eb {}; if (ctx == nullptr || n_mel_frames <= 4 || n_batch <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder(batched): invalid arg " - "(n_mel_frames=%d, n_batch=%d)\n", + "(n_mel_frames=%d, n_batch=%d)", n_mel_frames, n_batch); return eb; } @@ -379,7 +380,7 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, const int n_audio = T_enc / hp.proj_downsample; const int B = n_batch; if (n_audio <= 0) { - std::fprintf(stderr, "voxtral_realtime encoder(batched): T_enc=%d too small\n", T_enc); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder(batched): T_enc=%d too small", T_enc); return eb; } eb.T_enc = T_enc; @@ -450,7 +451,7 @@ EncoderBuildBatched build_encoder_graph_batched(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "voxtral_realtime encoder(batched): ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime encoder(batched): ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -464,7 +465,7 @@ EmbedderBuild build_embedder_graph(ggml_context * ctx, int n_mel_frames) { EmbedderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 4) { - std::fprintf(stderr, "voxtral_realtime embedder: invalid n_mel_frames=%d\n", n_mel_frames); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime embedder: invalid n_mel_frames=%d", n_mel_frames); return eb; } const int n_mels = hp.enc_num_mel_bins; @@ -487,7 +488,7 @@ EmbedderBuild build_embedder_graph(ggml_context * ctx, ggml_set_output(eb.out); eb.graph = ggml_new_graph_custom(ctx, /*size=*/2048, /*grads=*/false); - if (eb.graph == nullptr) { std::fprintf(stderr, "voxtral_realtime embedder: graph alloc failed\n"); return eb; } + if (eb.graph == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime embedder: graph alloc failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); return eb; } @@ -498,7 +499,7 @@ EmbedderChunkBuild build_embedder_chunk_graph(ggml_context * ctx, int n_new_mel) { EmbedderChunkBuild eb {}; if (ctx == nullptr || n_new_mel < 2 || (n_new_mel % 2) != 0) { - std::fprintf(stderr, "voxtral_realtime embedder chunk: invalid n_new_mel=%d\n", n_new_mel); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime embedder chunk: invalid n_new_mel=%d", n_new_mel); return eb; } const int n_mels = hp.enc_num_mel_bins; @@ -540,7 +541,7 @@ EmbedderChunkBuild build_embedder_chunk_graph(ggml_context * ctx, eb.graph = ggml_new_graph_custom(ctx, /*size=*/2048, /*grads=*/false); if (eb.graph == nullptr) { - std::fprintf(stderr, "voxtral_realtime embedder chunk: graph alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime embedder chunk: graph alloc failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); @@ -562,8 +563,8 @@ EncoderChunkBuild build_encoder_chunk_graph(ggml_context * ctx, if (ctx == nullptr || n_new <= 0 || (n_new % down) != 0 || write_slot < 0 || read_start < 0 || read_len <= 0 || read_start + read_len > enc_kv.n_ctx || write_slot + n_new > enc_kv.n_ctx || enc_kv.self_k == nullptr) { - std::fprintf(stderr, "voxtral_realtime enc-chunk: invalid arg (n_new=%d write_slot=%d " - "read_start=%d read_len=%d n_ctx=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime enc-chunk: invalid arg (n_new=%d write_slot=%d " + "read_start=%d read_len=%d n_ctx=%d)", n_new, write_slot, read_start, read_len, enc_kv.n_ctx); return cb; } @@ -584,7 +585,7 @@ EncoderChunkBuild build_encoder_chunk_graph(ggml_context * ctx, ggml_set_input(cb.mask_in); ggml_cgraph * gf = ggml_new_graph_custom(ctx, /*size=*/8192, /*grads=*/false); - if (gf == nullptr) { std::fprintf(stderr, "voxtral_realtime enc-chunk: graph alloc failed\n"); return cb; } + if (gf == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime enc-chunk: graph alloc failed"); return cb; } cb.graph = gf; ggml_tensor * x = cb.embed_in; // [d_model, n_new] diff --git a/src/arch/voxtral_realtime/model.cpp b/src/arch/voxtral_realtime/model.cpp index 5c59757b..56bf63ed 100644 --- a/src/arch/voxtral_realtime/model.cpp +++ b/src/arch/voxtral_realtime/model.cpp @@ -88,11 +88,11 @@ transcribe_status resolve_specials(const transcribe::Tokenizer & tok, out.streaming_pad = hp.streaming_pad_token_id; out.n_left_pad = 32; // tekken streaming_n_left_pad_tokens if (out.bos < 0 || out.eos < 0) { - std::fprintf(stderr, "voxtral_realtime: tokenizer missing bos/eos id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: tokenizer missing bos/eos id"); return TRANSCRIBE_ERR_GGUF; } if (out.streaming_pad < 0) { - std::fprintf(stderr, "voxtral_realtime: invalid streaming_pad token id\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: invalid streaming_pad token id"); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_OK; @@ -225,7 +225,7 @@ transcribe_status load(Loader & loader, m->hparams.bos_token_id = m->tok.bos_id(); m->hparams.eos_token_id = m->tok.eos_id(); if (m->hparams.vocab_size != m->hparams.dec_vocab_size) { - std::fprintf(stderr, "voxtral_realtime: tokenizer vocab (%d) != decoder vocab_size (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: tokenizer vocab (%d) != decoder vocab_size (%d)", m->hparams.vocab_size, m->hparams.dec_vocab_size); return TRANSCRIBE_ERR_GGUF; } @@ -287,7 +287,7 @@ transcribe_status load(Loader & loader, ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, "voxtral_realtime: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -483,7 +483,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, int k_drafts, std::string & out_text) { if (!cm->mel.has_value()) { - std::fprintf(stderr, "voxtral_realtime run: model has no MelFrontend\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: model has no MelFrontend"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -494,7 +494,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, const int64_t t_mel_start = ggml_time_us(); if (ref_mel_dir != nullptr && ref_mel_dir[0] != '\0') { if (!read_ref_mel(ref_mel_dir, n_mels, cc->mel_buf, mel_n_frames)) { - std::fprintf(stderr, "voxtral_realtime run: failed to read ref mel from %s\n", ref_mel_dir); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: failed to read ref mel from %s", ref_mel_dir); return TRANSCRIBE_ERR_GGUF; } } else { @@ -518,12 +518,12 @@ transcribe_status forward_buffer(Session * cc, Model * cm, int mm = 0; if (auto st = cm->mel->compute(padded.data(), total, cc->mel_buf, mm, mel_n_frames, cc->n_threads); st != TRANSCRIBE_OK) { - std::fprintf(stderr, "voxtral_realtime run: mel compute failed (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: mel compute failed (%s)", transcribe_status_string(st)); return st; } if (mm != n_mels) { - std::fprintf(stderr, "voxtral_realtime run: mel bins %d != %d\n", mm, n_mels); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: mel bins %d != %d", mm, n_mels); return TRANSCRIBE_ERR_GGUF; } } @@ -597,7 +597,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, } apply_threads(cc->sched, cc->n_threads); if (ggml_backend_sched_graph_compute(cc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run: encoder compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: encoder compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -716,7 +716,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, } apply_threads(cc->sched, cc->n_threads); if (ggml_backend_sched_graph_compute(cc->sched, pb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run: prefill compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: prefill compute failed"); return TRANSCRIBE_ERR_GGUF; } cc->kv_cache.n = T_prompt; cc->kv_cache.head = T_prompt; @@ -790,7 +790,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, ggml_backend_tensor_set(sb.mask_in, step_mask.data(), 0, static_cast(max_n_kv) * sizeof(ggml_fp16_t)); if (ggml_backend_sched_graph_compute(cc->sched, sb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run: step compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: step compute failed"); return TRANSCRIBE_ERR_GGUF; } int32_t tok = 0; @@ -892,7 +892,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, verify_mask.size() * sizeof(ggml_fp16_t)); if (ggml_backend_sched_graph_compute(cc->sched, vb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run: verify compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: verify compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -930,9 +930,9 @@ transcribe_status forward_buffer(Session * cc, Model * cm, // spin on the same cur_pos. if (static_cast(all_ids.size()) == prev_size) { if (!hit_eos) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "voxtral_realtime spec: no commit at cur_pos=%d " - "(n_audio=%d, predicted[0]=%d) — breaking\n", + "(n_audio=%d, predicted[0]=%d) — breaking", cur_pos, n_audio, predicted[0]); } break; @@ -997,7 +997,7 @@ transcribe_status forward_buffer(Session * cc, Model * cm, } apply_threads(cc->sched, cc->n_threads); if (ggml_backend_sched_graph_compute(cc->sched, tf.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run: teacher-forced dump compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: teacher-forced dump compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -1640,14 +1640,14 @@ transcribe_status stream_process(Session * cc, Model * cm, bool is_final, bool * const double enc_c = cc->stream_t_enc_compute_us / 1000.0; // pure graph_compute const double enc_o = enc - enc_c; // build + alloc + host prep const double tot = mel + conv + enc + dec; - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[stream-timing] mel=%.0fms (%.1f%%) conv=%.0fms (%.1f%%) enc=%.0fms (%.1f%%)" - " [compute=%.0f overhead=%.0f] dec=%.0fms (%.1f%%) sum=%.0fms\n", + " [compute=%.0f overhead=%.0f] dec=%.0fms (%.1f%%) sum=%.0fms", mel, tot > 0 ? 100.0 * mel / tot : 0.0, conv, tot > 0 ? 100.0 * conv / tot : 0.0, enc, tot > 0 ? 100.0 * enc / tot : 0.0, enc_c, enc_o, dec, tot > 0 ? 100.0 * dec / tot : 0.0, tot); - std::fprintf(stderr, - "[stream-mem] retained PCM=%zu samples (%.1fs) audio-embeds=%zu frames (vs %d total tokens)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[stream-mem] retained PCM=%zu samples (%.1fs) audio-embeds=%zu frames (vs %d total tokens)", cc->stream_pcm.size(), static_cast(cc->stream_pcm.size()) / std::max(1, hp.fe_sample_rate), cc->stream_audio_embeds.size() / std::max(1, dec_h), cc->stream_n_tok_ready); } @@ -1924,7 +1924,7 @@ transcribe_status run_batch_step_loop( ggml_backend_tensor_set(sb.kv_idx_in, kvidx_buf.data(), 0, kvidx_buf.size() * sizeof(int64_t)); ggml_backend_tensor_set(sb.mask_in, mask_buf.data(), 0, mask_buf.size() * sizeof(ggml_fp16_t)); if (ggml_backend_sched_graph_compute(cc->sched, sb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run_batch: step compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run_batch: step compute failed"); return TRANSCRIBE_ERR_GGUF; } ggml_backend_tensor_get(sb.out, out_buf.data(), 0, out_buf.size() * sizeof(int32_t)); @@ -2066,7 +2066,7 @@ transcribe_status run_batch(transcribe_session * session, const float * const * } apply_threads(cc->sched, cc->n_threads); if (ggml_backend_sched_graph_compute(cc->sched, eb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run_batch: encoder compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run_batch: encoder compute failed"); return TRANSCRIBE_ERR_GGUF; } @@ -2213,7 +2213,7 @@ transcribe_status run_batch(transcribe_session * session, const float * const * apply_threads(cc->sched, cc->n_threads); if (ggml_backend_sched_graph_compute(cc->sched, pb.graph) != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, "voxtral_realtime run_batch: prefill compute failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run_batch: prefill compute failed"); return TRANSCRIBE_ERR_GGUF; } std::vector first(n, 0); diff --git a/src/arch/voxtral_realtime/weights.cpp b/src/arch/voxtral_realtime/weights.cpp index b199deec..f1633d51 100644 --- a/src/arch/voxtral_realtime/weights.cpp +++ b/src/arch/voxtral_realtime/weights.cpp @@ -10,6 +10,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -73,7 +74,7 @@ transcribe_status read_hparams(const gguf_context * gguf, HParams & hp) { case KvResult::Ok: hp.streaming_pad_token_id = static_cast(spt); break; case KvResult::Absent: break; case KvResult::BadType: - std::fprintf(stderr, "voxtral_realtime: streaming_pad_token_id wrong type\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: streaming_pad_token_id wrong type"); return TRANSCRIBE_ERR_GGUF; } } @@ -100,51 +101,51 @@ transcribe_status read_hparams(const gguf_context * gguf, HParams & hp) { if (hp.enc_n_layers <= 0 || hp.enc_d_model <= 0 || hp.enc_n_heads <= 0 || hp.enc_head_dim <= 0 || hp.enc_ffn_dim <= 0 || hp.enc_num_mel_bins <= 0 || hp.enc_sliding_window <= 0) { - std::fprintf(stderr, "voxtral_realtime: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_n_heads != hp.enc_n_kv_heads) { - std::fprintf(stderr, "voxtral_realtime: encoder expects full MHA (n_heads==n_kv_heads)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: encoder expects full MHA (n_heads==n_kv_heads)"); return TRANSCRIBE_ERR_GGUF; } if (hp.proj_in != hp.enc_d_model * hp.proj_downsample) { - std::fprintf(stderr, "voxtral_realtime: projector input_dim (%d) != enc_d_model*downsample (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: projector input_dim (%d) != enc_d_model*downsample (%d)", hp.proj_in, hp.enc_d_model * hp.proj_downsample); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_layers <= 0 || hp.dec_hidden <= 0 || hp.dec_n_heads <= 0 || hp.dec_n_kv_heads <= 0 || hp.dec_head_dim <= 0 || hp.dec_intermediate <= 0) { - std::fprintf(stderr, "voxtral_realtime: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_n_heads % hp.dec_n_kv_heads != 0) { - std::fprintf(stderr, "voxtral_realtime: n_heads (%d) not divisible by n_kv_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: n_heads (%d) not divisible by n_kv_heads (%d)", hp.dec_n_heads, hp.dec_n_kv_heads); return TRANSCRIBE_ERR_GGUF; } if (!hp.dec_tie_word_embeddings) { - std::fprintf(stderr, "voxtral_realtime: decoder expects tied lm_head\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: decoder expects tied lm_head"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_hidden_act != "silu" && hp.dec_hidden_act != "swish") { - std::fprintf(stderr, "voxtral_realtime: unsupported decoder hidden_act \"%s\"\n", hp.dec_hidden_act.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: unsupported decoder hidden_act \"%s\"", hp.dec_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_hidden_act != "silu" && hp.enc_hidden_act != "swish") { - std::fprintf(stderr, "voxtral_realtime: unsupported encoder hidden_act \"%s\"\n", hp.enc_hidden_act.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: unsupported encoder hidden_act \"%s\"", hp.enc_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.proj_hidden_act != "gelu") { - std::fprintf(stderr, "voxtral_realtime: unsupported projector hidden_act \"%s\"\n", hp.proj_hidden_act.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: unsupported projector hidden_act \"%s\"", hp.proj_hidden_act.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.time_embed_dim != hp.dec_hidden) { - std::fprintf(stderr, "voxtral_realtime: time_embed_dim (%d) != dec_hidden (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: time_embed_dim (%d) != dec_hidden (%d)", hp.time_embed_dim, hp.dec_hidden); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, "voxtral_realtime: unsupported frontend type \"%s\"\n", hp.fe_type.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: unsupported frontend type \"%s\"", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } return TRANSCRIBE_OK; @@ -220,7 +221,7 @@ transcribe_status build_weights(ggml_context * ctx_meta, GET_LIN(b.ffn_down_w, lname("enc.blocks.%d.ffn.down.weight", i), enc_ff, d_model); GET_F32(b.ffn_down_b, lname("enc.blocks.%d.ffn.down.bias", i), d_model); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, "voxtral_realtime: enc ffn gate/up dtype mismatch at layer %d\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: enc ffn gate/up dtype mismatch at layer %d", i); return TRANSCRIBE_ERR_GGUF; } } @@ -258,7 +259,7 @@ transcribe_status build_weights(ggml_context * ctx_meta, GET_LIN(b.ada_linear_1_w, lname("dec.blocks.%d.ada.linear_1.weight", i), dec_h, ada_h); GET_LIN(b.ada_linear_2_w, lname("dec.blocks.%d.ada.linear_2.weight", i), ada_h, dec_h); if (b.ffn_gate_w->type != b.ffn_up_w->type) { - std::fprintf(stderr, "voxtral_realtime: dec ffn gate/up dtype mismatch at layer %d\n", i); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime: dec ffn gate/up dtype mismatch at layer %d", i); return TRANSCRIBE_ERR_GGUF; } } diff --git a/src/arch/whisper/bin_load.cpp b/src/arch/whisper/bin_load.cpp index c7291934..73de5d98 100644 --- a/src/arch/whisper/bin_load.cpp +++ b/src/arch/whisper/bin_load.cpp @@ -15,6 +15,7 @@ #include "transcribe-bin-loader.h" #include "transcribe-load-common.h" #include "transcribe-tokenizer.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-alloc.h" @@ -374,8 +375,8 @@ ggml_tensor * create_canonical_tensor( ggml_tensor * t = ggml_new_tensor(ctx_meta, src.type, n_dims, ne); if (t == nullptr) { - std::fprintf(stderr, - "%s: ggml_new_tensor failed for \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: ggml_new_tensor failed for \"%s\"", kTag, canonical_name.c_str()); return nullptr; } @@ -395,8 +396,8 @@ transcribe_status resolve_tensors( by_name.reserve(bm.tensors.size() * 2); for (const auto & e : bm.tensors) { if (!by_name.emplace(e.name, &e).second) { - std::fprintf(stderr, - "%s: duplicate tensor \"%s\" in .bin\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: duplicate tensor \"%s\" in .bin", kTag, e.name.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -430,9 +431,9 @@ transcribe_status resolve_tensors( auto add_slot = [&](const StaticRename & rule) -> transcribe_status { auto it = by_name.find(rule.legacy); if (it == by_name.end()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: missing tensor \"%s\" (canonical \"%s\") " - "— .bin is incomplete or not a whisper model\n", + "— .bin is incomplete or not a whisper model", kTag, rule.legacy, rule.canonical); return TRANSCRIBE_ERR_GGUF; } @@ -480,9 +481,9 @@ transcribe_status resolve_tensors( if (consumed.size() != bm.tensors.size()) { for (const auto & e : bm.tensors) { if (consumed.find(e.name) == consumed.end()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: unconsumed .bin tensor \"%s\" — " - "this loader does not yet support it\n", + "this loader does not yet support it", kTag, e.name.c_str()); } } @@ -696,8 +697,8 @@ transcribe_status load_from_bin(const char * path, ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (buf == nullptr) { - std::fprintf(stderr, - "%s: ggml_backend_alloc_ctx_tensors failed\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: ggml_backend_alloc_ctx_tensors failed", kTag); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = buf; @@ -727,9 +728,9 @@ transcribe_status load_from_bin(const char * path, const size_t want = ggml_nbytes(m->weights.frontend.mel_filterbank); const size_t have = filterbank.size() * sizeof(float); if (have != want) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: mel filterbank size mismatch " - "(have %zu bytes, expected %zu)\n", + "(have %zu bytes, expected %zu)", kTag, have, want); return TRANSCRIBE_ERR_GGUF; } @@ -741,9 +742,9 @@ transcribe_status load_from_bin(const char * path, const size_t want = ggml_nbytes(m->weights.frontend.window); const size_t have = window.size() * sizeof(float); if (have != want) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: hann window size mismatch " - "(have %zu bytes, expected %zu)\n", + "(have %zu bytes, expected %zu)", kTag, have, want); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/arch/whisper/decoder.cpp b/src/arch/whisper/decoder.cpp index 879c9d95..33c7dbe7 100644 --- a/src/arch/whisper/decoder.cpp +++ b/src/arch/whisper/decoder.cpp @@ -26,6 +26,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -491,16 +492,16 @@ DecoderBuild build_decoder_prefill_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || seq_len <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper decoder: invalid arg " - "(ctx=%p, seq_len=%d, T_enc=%d)\n", + "(ctx=%p, seq_len=%d, T_enc=%d)", static_cast(ctx), seq_len, T_enc); return db; } if (seq_len > hp.dec_max_target_positions) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper decoder: seq_len=%d exceeds " - "max_target_positions=%d\n", + "max_target_positions=%d", seq_len, hp.dec_max_target_positions); return db; } @@ -600,8 +601,8 @@ DecoderBuild build_decoder_prefill_graph(ggml_context * ctx, // ~20 ops with self+cross+ffn). db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "whisper decoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper decoder: ggml_new_graph_custom failed"); return db; } ggml_build_forward_expand(db.graph, db.out); @@ -627,9 +628,9 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || encoder_out == nullptr || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper cross_kv: invalid arg " - "(ctx=%p, encoder_out=%p, T_enc=%d)\n", + "(ctx=%p, encoder_out=%p, T_enc=%d)", static_cast(ctx), static_cast(encoder_out), T_enc); return db; @@ -647,8 +648,8 @@ DecoderBuild build_cross_kv_graph(ggml_context * ctx, db.graph = ggml_new_graph_custom(ctx, 4096, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "whisper cross_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper cross_kv: ggml_new_graph_custom failed"); return db; } @@ -711,16 +712,16 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, DecoderBuild db {}; if (ctx == nullptr || n_tokens <= 0 || T_enc <= 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper decoder_kv: invalid arg " - "(ctx=%p, n_tokens=%d, T_enc=%d)\n", + "(ctx=%p, n_tokens=%d, T_enc=%d)", static_cast(ctx), n_tokens, T_enc); return db; } const int n_kv_active = n_past + n_tokens; if (n_kv_active > kv_cache.n_ctx) { - std::fprintf(stderr, - "whisper decoder_kv: n_kv=%d exceeds n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper decoder_kv: n_kv=%d exceeds n_ctx=%d", n_kv_active, kv_cache.n_ctx); return db; } @@ -827,8 +828,8 @@ DecoderBuild build_decoder_graph_kv(ggml_context * ctx, // ggml_build_forward_expand to wire the cache writes into it. db.graph = ggml_new_graph_custom(ctx, 8192, false); if (db.graph == nullptr) { - std::fprintf(stderr, - "whisper decoder_kv: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper decoder_kv: ggml_new_graph_custom failed"); return db; } @@ -928,14 +929,14 @@ StepBuild build_step_graph(ggml_context * ctx, sb.max_n_kv = max_n_kv; if (ctx == nullptr || max_n_kv <= 0 || T_enc <= 0) { - std::fprintf(stderr, - "whisper step: invalid arg (max_n_kv=%d, T_enc=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper step: invalid arg (max_n_kv=%d, T_enc=%d)", max_n_kv, T_enc); return sb; } if (max_n_kv > kv_cache.n_ctx) { - std::fprintf(stderr, - "whisper step: max_n_kv=%d exceeds kv_cache.n_ctx=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper step: max_n_kv=%d exceeds kv_cache.n_ctx=%d", max_n_kv, kv_cache.n_ctx); return sb; } @@ -972,7 +973,7 @@ StepBuild build_step_graph(ggml_context * ctx, sb.graph = ggml_new_graph_custom(ctx, 8192, false); if (sb.graph == nullptr) { - std::fprintf(stderr, "whisper step: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper step: ggml_new_graph_custom failed"); return sb; } @@ -1203,7 +1204,7 @@ StepBuildBatched build_step_graph_batched(ggml_context * ctx, sb.n_batch = n_batch; if (ctx == nullptr || max_n_kv <= 0 || T_enc_max <= 0 || n_batch <= 0) return sb; if (!use_flash) { - std::fprintf(stderr, "whisper step(batched): requires flash path\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper step(batched): requires flash path"); return sb; } diff --git a/src/arch/whisper/encoder.cpp b/src/arch/whisper/encoder.cpp index 6902fa7e..51fa6b7d 100644 --- a/src/arch/whisper/encoder.cpp +++ b/src/arch/whisper/encoder.cpp @@ -32,6 +32,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-log.h" #include "ggml.h" @@ -198,15 +199,15 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, EncoderBuild eb {}; if (ctx == nullptr || n_mel_frames <= 0) { - std::fprintf(stderr, - "whisper encoder: invalid arg (ctx=%p, n_mel_frames=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper encoder: invalid arg (ctx=%p, n_mel_frames=%d)", static_cast(ctx), n_mel_frames); return eb; } if (n_mel_frames % 2 != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper encoder: n_mel_frames=%d must be even " - "(stride-2 conv2 would produce fractional T_enc)\n", + "(stride-2 conv2 would produce fractional T_enc)", n_mel_frames); return eb; } @@ -217,8 +218,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, const int T_enc = n_mel_frames / 2; if (T_enc > hp.enc_max_source_positions) { - std::fprintf(stderr, - "whisper encoder: T_enc=%d exceeds max_source_positions=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper encoder: T_enc=%d exceeds max_source_positions=%d", T_enc, hp.enc_max_source_positions); return eb; } @@ -327,8 +328,8 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // go up to 32 (large). 8192 leaves headroom for the block ops. eb.graph = ggml_new_graph_custom(ctx, 8192, false); if (eb.graph == nullptr) { - std::fprintf(stderr, - "whisper encoder: ggml_new_graph_custom failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper encoder: ggml_new_graph_custom failed"); return eb; } ggml_build_forward_expand(eb.graph, eb.out); diff --git a/src/arch/whisper/model.cpp b/src/arch/whisper/model.cpp index 01b908e7..40b8d6ce 100644 --- a/src/arch/whisper/model.cpp +++ b/src/arch/whisper/model.cpp @@ -18,6 +18,7 @@ #include "transcribe-load-common.h" #include "transcribe-loader.h" #include "transcribe-meta.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-alloc.h" @@ -97,7 +98,7 @@ bool enc_out_init(WhisperEncOut & enc_out, enc_out.ctx = ggml_init(params); if (enc_out.ctx == nullptr) { - std::fprintf(stderr, "whisper enc_out: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper enc_out: ggml_init failed"); return false; } @@ -107,7 +108,7 @@ bool enc_out_init(WhisperEncOut & enc_out, enc_out.buffer = ggml_backend_alloc_ctx_tensors(enc_out.ctx, backend); if (enc_out.buffer == nullptr) { - std::fprintf(stderr, "whisper enc_out: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper enc_out: buffer alloc failed"); ggml_free(enc_out.ctx); enc_out.ctx = nullptr; enc_out.tensor = nullptr; @@ -138,9 +139,9 @@ bool kv_cache_init(WhisperKvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper kv_cache: unsupported kv_type=%d " - "(only F16/F32)\n", static_cast(kv_type)); + "(only F16/F32)", static_cast(kv_type)); return false; } @@ -152,7 +153,7 @@ bool kv_cache_init(WhisperKvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "whisper kv_cache: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper kv_cache: ggml_init failed"); return false; } @@ -179,7 +180,7 @@ bool kv_cache_init(WhisperKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "whisper kv_cache: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper kv_cache: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -213,8 +214,8 @@ bool kv_cache_init_batched(WhisperKvCache & cache, n_layer, kv_type); } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, - "whisper kv_cache(batched): unsupported kv_type=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper kv_cache(batched): unsupported kv_type=%d", static_cast(kv_type)); return false; } @@ -229,7 +230,7 @@ bool kv_cache_init_batched(WhisperKvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "whisper kv_cache(batched): ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper kv_cache(batched): ggml_init failed"); return false; } @@ -253,7 +254,7 @@ bool kv_cache_init_batched(WhisperKvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "whisper kv_cache(batched): buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper kv_cache(batched): buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -344,35 +345,35 @@ void print_whisper_perf(const WhisperPerf & p) { stage_total(p.step_compute) + stage_total(p.step_tensor_get) + stage_total(p.step_cpu); - std::fprintf(stderr, - "[whisper-perf] chunks=%d encs=%d crosses=%d prompts=%d steps=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, + "[whisper-perf] chunks=%d encs=%d crosses=%d prompts=%d steps=%d", p.chunks, p.enc_compute.count, p.cross_compute.count, p.prompt_compute.count, p.step_compute.count); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] enc total=%7.2f ms build=%7.2f alloc=%7.2f " - "compute=%7.2f tget=%7.2f\n", + "compute=%7.2f tget=%7.2f", ms(enc_total), ms(p.enc_build.total_us), ms(p.enc_alloc.total_us), ms(p.enc_compute.total_us), ms(p.enc_tensor_get.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] cross total=%7.2f ms build=%7.2f alloc=%7.2f " - "compute=%7.2f\n", + "compute=%7.2f", ms(cross_total), ms(p.cross_build.total_us), ms(p.cross_alloc.total_us), ms(p.cross_compute.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] prompt total=%7.2f ms build=%7.2f alloc=%7.2f " - "compute=%7.2f tget=%7.2f cpu=%7.2f\n", + "compute=%7.2f tget=%7.2f cpu=%7.2f", ms(prompt_total), ms(p.prompt_build.total_us), ms(p.prompt_alloc.total_us), ms(p.prompt_compute.total_us), ms(p.prompt_tensor_get.total_us), ms(p.prompt_cpu.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] step total=%7.2f ms build=%7.2f alloc=%7.2f " - "compute=%7.2f tget=%7.2f cpu=%7.2f\n", + "compute=%7.2f tget=%7.2f cpu=%7.2f", ms(step_total), ms(p.step_build.total_us), ms(p.step_alloc.total_us), ms(p.step_compute.total_us), ms(p.step_tensor_get.total_us), ms(p.step_cpu.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] step avg us build=%6.1f alloc=%6.1f " - "compute=%6.1f tget=%6.1f cpu=%6.1f\n", + "compute=%6.1f tget=%6.1f cpu=%6.1f", avg_us(p.step_build), avg_us(p.step_alloc), avg_us(p.step_compute), avg_us(p.step_tensor_get), avg_us(p.step_cpu)); @@ -391,23 +392,23 @@ void print_whisper_perf(const WhisperPerf & p) { } } if (show_cpu_breakdown) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] prompt cpu suppress=%7.2f ts=%7.2f " - "sample=%7.2f logprob=%7.2f (ms)\n", + "sample=%7.2f logprob=%7.2f (ms)", ms(p.prompt_cpu_suppress.total_us), ms(p.prompt_cpu_timestamp.total_us), ms(p.prompt_cpu_sample.total_us), ms(p.prompt_cpu_logprob.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] step cpu suppress=%7.2f ts=%7.2f " - "sample=%7.2f logprob=%7.2f (ms)\n", + "sample=%7.2f logprob=%7.2f (ms)", ms(p.step_cpu_suppress.total_us), ms(p.step_cpu_timestamp.total_us), ms(p.step_cpu_sample.total_us), ms(p.step_cpu_logprob.total_us)); - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "[whisper-perf] step avg us suppress=%6.1f ts=%6.1f " - "sample=%6.1f logprob=%6.1f\n", + "sample=%6.1f logprob=%6.1f", avg_us(p.step_cpu_suppress), avg_us(p.step_cpu_timestamp), avg_us(p.step_cpu_sample), avg_us(p.step_cpu_logprob)); } @@ -543,8 +544,8 @@ transcribe_status whisper_load( ggml_backend_alloc_ctx_tensors(m->ctx_meta, m->plan.primary); if (weights_buffer == nullptr) { gguf_free(gguf_data); - std::fprintf(stderr, - "whisper: ggml_backend_alloc_ctx_tensors failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: ggml_backend_alloc_ctx_tensors failed"); return TRANSCRIBE_ERR_GGUF; } m->backend_buffer = weights_buffer; @@ -612,9 +613,9 @@ transcribe_status whisper_load( const std::string piece = std::string("<|") + code + "|>"; const int id = m->tok.find(piece); if (id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: language '%s' has no '%s' token " - "in tokenizer vocab\n", + "in tokenizer vocab", code, piece.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -696,8 +697,8 @@ transcribe_status run_whisper_encoder_on_window( // ggml_reset between calls instead of free + reinit. const int64_t t_enc_build_start = ggml_time_us(); if (!ensure_compute_ctx(cc, 8 * 1024 * 1024)) { - std::fprintf(stderr, - "whisper run: ensure_compute_ctx (encoder) failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: ensure_compute_ctx (encoder) failed"); return TRANSCRIBE_ERR_GGUF; } @@ -725,8 +726,8 @@ transcribe_status run_whisper_encoder_on_window( if (!enc_out_init(cc->enc_out, cm->plan.primary, d_enc_g, T_enc_g)) { - std::fprintf(stderr, - "whisper run: enc_out_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: enc_out_init failed"); return TRANSCRIBE_ERR_GGUF; } } @@ -748,16 +749,16 @@ transcribe_status run_whisper_encoder_on_window( static_cast(cm->plan.scheduler_list.size()), 16384, false, true); if (cc->sched == nullptr) { - std::fprintf(stderr, - "whisper run: ggml_backend_sched_new failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: ggml_backend_sched_new failed"); return TRANSCRIBE_ERR_GGUF; } } ggml_backend_sched_reset(cc->sched); if (!ggml_backend_sched_alloc_graph(cc->sched, eb.graph)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: ggml_backend_sched_alloc_graph failed " - "(encoder)\n"); + "(encoder)"); return TRANSCRIBE_ERR_GGUF; } @@ -774,8 +775,8 @@ transcribe_status run_whisper_encoder_on_window( ggml_backend_sched_graph_compute(cc->sched, eb.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "whisper run: encoder graph compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: encoder graph compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -1038,8 +1039,8 @@ transcribe_status load_mel_from_ref(const char * ref_dir, std::ifstream f(path, std::ios::binary); if (!f) { - std::fprintf(stderr, - "whisper run: cannot open mel ref '%s'\n", path.c_str()); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: cannot open mel ref '%s'", path.c_str()); return TRANSCRIBE_ERR_FILE_NOT_FOUND; } @@ -1051,9 +1052,9 @@ transcribe_status load_mel_from_ref(const char * ref_dir, f.read(reinterpret_cast(out.data()), static_cast(n_bytes)); if (static_cast(f.gcount()) != n_bytes) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: short read on mel ref '%s' " - "(got %lld bytes, want %zu)\n", + "(got %lld bytes, want %zu)", path.c_str(), static_cast(f.gcount()), n_bytes); return TRANSCRIBE_ERR_GGUF; } @@ -1374,9 +1375,9 @@ transcribe_status whisper_run( total_mel_frames = n_mel_frames_per_chunk; } else { if (!cm->mel.has_value()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: model has no MelFrontend " - "(load skipped?)\n"); + "(load skipped?)"); return TRANSCRIBE_ERR_GGUF; } @@ -1398,21 +1399,21 @@ transcribe_status whisper_run( pcm_in, pcm_in_n, mel_mn, mel_n_mels, mel_n_frames); mst != TRANSCRIBE_OK) { - std::fprintf(stderr, - "whisper run: MelFrontend::compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: MelFrontend::compute failed (%d)", static_cast(mst)); return mst; } if (mel_n_mels != n_mels) { - std::fprintf(stderr, - "whisper run: mel n_mels %d != expected %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: mel n_mels %d != expected %d", mel_n_mels, n_mels); return TRANSCRIBE_ERR_GGUF; } if (is_short_form && mel_n_frames != n_mel_frames_per_chunk) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: short-form mel has %d frames, " - "expected %d\n", + "expected %d", mel_n_frames, n_mel_frames_per_chunk); return TRANSCRIBE_ERR_GGUF; } @@ -1470,9 +1471,9 @@ transcribe_status whisper_run( return TRANSCRIBE_ERR_GGUF; } } else if (params != nullptr && params->task == TRANSCRIBE_TASK_TRANSLATE) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: this model does not support translate " - "(non-multilingual variant)\n"); + "(non-multilingual variant)"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1488,9 +1489,9 @@ transcribe_status whisper_run( const std::string lang_code = params->language; if (!is_multilingual) { if (lang_code != "en") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: language '%s' is not supported by " - "this non-multilingual model (only 'en')\n", + "this non-multilingual model (only 'en')", lang_code.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1503,9 +1504,9 @@ transcribe_status whisper_run( } } if (lang_token < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: language '%s' not in model's language " - "table — cannot build decoder prompt\n", + "table — cannot build decoder prompt", lang_code.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1651,9 +1652,9 @@ transcribe_status whisper_run( if (wp->prompt_condition == TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS && !wp->condition_on_prev_tokens) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: prompt_condition=ALL_SEGMENTS requires " - "condition_on_prev_tokens=true (HF parity)\n"); + "condition_on_prev_tokens=true (HF parity)"); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1670,9 +1671,9 @@ transcribe_status whisper_run( if (prev_sot_id < 0 && (prompt_requested || wp->condition_on_prev_tokens)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: model has no <|startofprev|> token; " - "initial_prompt / condition_on_prev_tokens unavailable\n"); + "initial_prompt / condition_on_prev_tokens unavailable"); return TRANSCRIBE_ERR_GGUF; } @@ -1701,10 +1702,10 @@ transcribe_status whisper_run( // a malformed prefix. (Header documents this contract; // runtime check makes it observable.) if (prev_sot_id >= 0 && wp->prompt_tokens[0] == prev_sot_id) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: prompt_tokens must not include " "<|startofprev|> (id %d) at index 0; the library " - "prepends it. See transcribe_whisper_run_ext docs.\n", + "prepends it. See transcribe_whisper_run_ext docs.", prev_sot_id); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1742,9 +1743,9 @@ transcribe_status whisper_run( std::string piece = text.substr(i, close - i); const int id = cm->tok.find(piece); if (id >= eos_id) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: initial_prompt contains " - "disallowed special token \"%s\" (id %d)\n", + "disallowed special token \"%s\" (id %d)", piece.c_str(), id); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -1756,16 +1757,16 @@ transcribe_status whisper_run( } if (cm->tok.encode(text, prompt_text_ids) != TRANSCRIBE_OK) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: tokenizer.encode failed on " - "initial_prompt\n"); + "initial_prompt"); return TRANSCRIBE_ERR_GGUF; } for (int32_t id : prompt_text_ids) { if (id >= eos_id) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: initial_prompt contains " - "disallowed special token id %d\n", id); + "disallowed special token id %d", id); return TRANSCRIBE_ERR_INVALID_ARG; } } @@ -1918,9 +1919,9 @@ transcribe_status whisper_run( } } if (is_multilingual && lang_token < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: could not resolve a decoder language " - "token\n"); + "token"); return TRANSCRIBE_ERR_GGUF; } @@ -2005,9 +2006,9 @@ transcribe_status whisper_run( // pathological caller passing prompt_tokens > 444 would land // here. if (seq_len + 1 > static_cast(n_ctx_decoder)) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: prefix length %d exceeds decoder " - "context %lld\n", + "context %lld", seq_len, static_cast(n_ctx_decoder)); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -2165,7 +2166,7 @@ transcribe_status whisper_run( static_cast(n_ctx_decoder), T_enc_local, cm->hparams.dec_d_model, n_layers, kv_type_g)) { - std::fprintf(stderr, "whisper run: KV cache init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: KV cache init failed"); return TRANSCRIBE_ERR_BACKEND; } } @@ -2180,8 +2181,8 @@ transcribe_status whisper_run( { const int64_t t_cross_build_start = ggml_time_us(); if (!new_compute_ctx(8 * 1024 * 1024)) { - std::fprintf(stderr, - "whisper run: ggml_init for cross_kv failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: ggml_init for cross_kv failed"); return TRANSCRIBE_ERR_GGUF; } DecoderBuild cross_db = build_cross_kv_graph( @@ -2195,8 +2196,8 @@ transcribe_status whisper_run( const int64_t t_cross_alloc_start = ggml_time_us(); ggml_backend_sched_reset(cc->sched); if (!ggml_backend_sched_alloc_graph(cc->sched, cross_db.graph)) { - std::fprintf(stderr, - "whisper run: alloc_graph failed (cross_kv)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: alloc_graph failed (cross_kv)"); return TRANSCRIBE_ERR_GGUF; } // No tensor_set: cross-KV reads cc->enc_out.tensor via @@ -2209,8 +2210,8 @@ transcribe_status whisper_run( cc->sched, cross_db.graph); gs != GGML_STATUS_SUCCESS) { - std::fprintf(stderr, - "whisper run: cross_kv compute failed (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: cross_kv compute failed (%d)", static_cast(gs)); return TRANSCRIBE_ERR_GGUF; } @@ -2266,8 +2267,8 @@ transcribe_status whisper_run( const int64_t t_prompt_alloc_start = ggml_time_us(); ggml_backend_sched_reset(cc->sched); if (!ggml_backend_sched_alloc_graph(cc->sched, db.graph)) { - std::fprintf(stderr, - "whisper run: alloc_graph failed (prompt)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: alloc_graph failed (prompt)"); return TRANSCRIBE_ERR_GGUF; } @@ -2497,14 +2498,14 @@ transcribe_status whisper_run( cc->compute_ctx, cm->weights, cm->hparams, cc->kv_cache, max_n_kv, T_enc_local, cc->decoder_use_flash); if (sb.graph == nullptr || sb.logits_out == nullptr) { - std::fprintf(stderr, - "whisper run: build_step_graph failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: build_step_graph failed"); return TRANSCRIBE_ERR_GGUF; } ggml_backend_sched_reset(cc->sched); if (!ggml_backend_sched_alloc_graph(cc->sched, sb.graph)) { - std::fprintf(stderr, - "whisper run: sched_alloc_graph failed (step)\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper run: sched_alloc_graph failed (step)"); return TRANSCRIBE_ERR_GGUF; } @@ -2542,8 +2543,8 @@ transcribe_status whisper_run( if (n_past + 1 > static_cast(n_ctx_decoder)) break; if (use_step_graph && n_past + 1 > max_n_kv) { - std::fprintf(stderr, - "whisper run: hit max_n_kv=%d at n_past=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, + "whisper run: hit max_n_kv=%d at n_past=%d", max_n_kv, n_past); break; } @@ -3302,7 +3303,7 @@ transcribe_status whisper_run_batch( if (cc->kv_cache.buffer == nullptr) { if (!kv_cache_init_batched(cc->kv_cache, cm->plan.primary, max_n_kv, T_enc_max, d_model, n_layer, B, kv_type_g)) - { std::fprintf(stderr, "whisper run_batch: kv_cache_init_batched failed\n"); + { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run_batch: kv_cache_init_batched failed"); return TRANSCRIBE_ERR_BACKEND; } } else { ggml_backend_buffer_clear(cc->kv_cache.buffer, 0); @@ -3610,10 +3611,10 @@ transcribe_status whisper_run_batch( if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { int n_serial = 0; for (int b = 0; b < n; ++b) n_serial += needs_serial[b] ? 1 : 0; - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "whisper run_batch: n=%d batched=%d serial=%d tiers=%d want_ts=%d " "T_enc_max=%d kv_window=%d (cap %d) prompt=%d\n" - " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)\n", + " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)", n, n_batch_active, n_serial, static_cast(temperatures.size()), want_ts ? 1 : 0, T_enc_max, kv_window, max_n_kv, prompt_len, mel_us / 1000.0, enc_us / 1000.0, n_batch_active, dec_us / 1000.0); diff --git a/src/arch/whisper/weights.cpp b/src/arch/whisper/weights.cpp index 0d988714..c6eac588 100644 --- a/src/arch/whisper/weights.cpp +++ b/src/arch/whisper/weights.cpp @@ -29,6 +29,7 @@ #include "transcribe-meta.h" #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" #include "gguf.h" @@ -58,7 +59,7 @@ transcribe_status read_optional_u32_kv(const gguf_context * gguf, if (st == transcribe::KvResult::Absent) { return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -77,7 +78,7 @@ transcribe_status read_optional_i32_array_kv(const gguf_context * gguf, if (st == transcribe::KvResult::Absent) { return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type (expected int32 array)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type (expected int32 array)", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -99,7 +100,7 @@ transcribe_status read_optional_f32_kv(const gguf_context * gguf, out = default_value; return TRANSCRIBE_OK; } - std::fprintf(stderr, "%s: KV %s has wrong type\n", error_tag, key); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: KV %s has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -187,12 +188,12 @@ transcribe_status read_whisper_hparams(const gguf_context * gguf, hp.enc_ffn_dim <= 0 || hp.enc_num_mel_bins <= 0 || hp.enc_max_source_positions <= 0) { - std::fprintf(stderr, "whisper: encoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: encoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model % hp.enc_n_heads != 0) { - std::fprintf(stderr, - "whisper: encoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: encoder d_model (%d) not divisible by n_heads (%d)", hp.enc_d_model, hp.enc_n_heads); return TRANSCRIBE_ERR_GGUF; } @@ -200,28 +201,28 @@ transcribe_status read_whisper_hparams(const gguf_context * gguf, hp.dec_ffn_dim <= 0 || hp.dec_max_target_positions <= 0 || hp.dec_vocab_size <= 0) { - std::fprintf(stderr, "whisper: decoder hparams must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: decoder hparams must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_d_model % hp.dec_n_heads != 0) { - std::fprintf(stderr, - "whisper: decoder d_model (%d) not divisible by n_heads (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: decoder d_model (%d) not divisible by n_heads (%d)", hp.dec_d_model, hp.dec_n_heads); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_d_model != hp.dec_d_model) { // Upstream whisper always uses d_model=d_model; a mismatch would // break the cross-attention K/V dim contract. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: encoder d_model (%d) != decoder d_model (%d); " - "cross-attention would mismatch\n", + "cross-attention would mismatch", hp.enc_d_model, hp.dec_d_model); return TRANSCRIBE_ERR_GGUF; } if (hp.enc_activation != "gelu" || hp.dec_activation != "gelu") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: only \"gelu\" activation is supported " - "(enc=\"%s\", dec=\"%s\")\n", + "(enc=\"%s\", dec=\"%s\")", hp.enc_activation.c_str(), hp.dec_activation.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -229,10 +230,10 @@ transcribe_status read_whisper_hparams(const gguf_context * gguf, // Whisper always ties lm_head to token_embedding; a false here // would mean the converter shipped a separate head tensor we // don't currently load. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: stt.whisper.decoder.tie_word_embeddings=false " "is not supported (no separate lm_head tensor in the " - "catalog)\n"); + "catalog)"); return TRANSCRIBE_ERR_GGUF; } if (hp.dec_scale_embedding) { @@ -240,77 +241,77 @@ transcribe_status read_whisper_hparams(const gguf_context * gguf, // config.scale_embedding=True. Upstream Whisper always ships False; // if this ever flips we need to add the scale to the decoder graph // (both prefill and KV paths, after get_rows(token_embd, ...)). - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: stt.whisper.decoder.scale_embedding=true is " "not supported (decoder graph does not apply the " - "sqrt(d_model) embed scale)\n"); + "sqrt(d_model) embed scale)"); return TRANSCRIBE_ERR_GGUF; } if (hp.decoder_start_token_id < 0 || hp.no_timestamps_token_id < 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: decoder_start_token_id / no_timestamps_token_id " - "must be set\n"); + "must be set"); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_num_mels <= 0 || hp.fe_sample_rate <= 0 || hp.fe_n_fft <= 0 || hp.fe_win_length <= 0 || hp.fe_hop_length <= 0) { - std::fprintf(stderr, "whisper: frontend dimensions must be positive\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: frontend dimensions must be positive"); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_win_length != hp.fe_n_fft) { // Whisper uses a full-length periodic Hann window (win=n_fft=400). // A mismatch would require a windowed-shorter-than-FFT code path // that we don't implement. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: frontend win_length (%d) != n_fft (%d); " - "only full-length window is supported\n", + "only full-length window is supported", hp.fe_win_length, hp.fe_n_fft); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_num_mels != hp.enc_num_mel_bins) { - std::fprintf(stderr, - "whisper: frontend num_mels (%d) != encoder num_mel_bins (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: frontend num_mels (%d) != encoder num_mel_bins (%d)", hp.fe_num_mels, hp.enc_num_mel_bins); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_type != "mel") { - std::fprintf(stderr, - "whisper: unsupported frontend type \"%s\" (only \"mel\")\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: unsupported frontend type \"%s\" (only \"mel\")", hp.fe_type.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_window != "hann" && hp.fe_window != "hann_periodic") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: unsupported frontend window \"%s\" " - "(only \"hann\"/\"hann_periodic\" is supported)\n", + "(only \"hann\"/\"hann_periodic\" is supported)", hp.fe_window.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_normalize != "whisper_logmel") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: unsupported frontend normalize \"%s\" " - "(only \"whisper_logmel\" is supported)\n", + "(only \"whisper_logmel\" is supported)", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_mel_norm != "slaney") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: unsupported frontend mel_norm \"%s\" " - "(only \"slaney\" is supported)\n", + "(only \"slaney\" is supported)", hp.fe_mel_norm.c_str()); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_pad_mode != "reflect") { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: unsupported frontend pad_mode \"%s\" " - "(only \"reflect\" is supported)\n", + "(only \"reflect\" is supported)", hp.fe_pad_mode.c_str()); return TRANSCRIBE_ERR_GGUF; } if (!hp.fe_center) { - std::fprintf(stderr, - "whisper: non-centered STFT is not supported\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: non-centered STFT is not supported"); return TRANSCRIBE_ERR_GGUF; } if (hp.fe_dither != 0.0f) { @@ -319,9 +320,9 @@ transcribe_status read_whisper_hparams(const gguf_context * gguf, // A non-zero value in the GGUF would be silently dropped, so // reject it at load time rather than producing a silently-wrong // spectrogram. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper: stt.frontend.dither=%g is not supported " - "(frontend is deterministic; expected 0.0)\n", + "(frontend is deterministic; expected 0.0)", hp.fe_dither); return TRANSCRIBE_ERR_GGUF; } @@ -360,8 +361,8 @@ transcribe_status install_mel_from_buffers( { cfg.normalize = "per_utterance"; } else { - std::fprintf(stderr, - "whisper: unsupported fe_normalize='%s'\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "whisper: unsupported fe_normalize='%s'", hp.fe_normalize.c_str()); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/causal_lm/causal_lm.cpp b/src/causal_lm/causal_lm.cpp index f8056c3b..96a82275 100644 --- a/src/causal_lm/causal_lm.cpp +++ b/src/causal_lm/causal_lm.cpp @@ -9,6 +9,7 @@ #include "causal_lm.h" #include "transcribe-session.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-backend.h" @@ -77,8 +78,8 @@ bool kv_init(KvCache & cache, ggml_type kv_type) { if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, - "causal_lm kv_init: unsupported kv_type=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "causal_lm kv_init: unsupported kv_type=%d", static_cast(kv_type)); return false; } @@ -91,7 +92,7 @@ bool kv_init(KvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "causal_lm kv_init: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "causal_lm kv_init: ggml_init failed"); return false; } @@ -104,7 +105,7 @@ bool kv_init(KvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, "causal_lm kv_init: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "causal_lm kv_init: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -136,8 +137,8 @@ bool kv_init_batched(KvCache & cache, return true; } if (kv_type != GGML_TYPE_F16 && kv_type != GGML_TYPE_F32) { - std::fprintf(stderr, - "causal_lm kv_init_batched: unsupported kv_type=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "causal_lm kv_init_batched: unsupported kv_type=%d", static_cast(kv_type)); return false; } @@ -150,7 +151,7 @@ bool kv_init_batched(KvCache & cache, cache.ctx = ggml_init(params); if (cache.ctx == nullptr) { - std::fprintf(stderr, "causal_lm kv_init_batched: ggml_init failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "causal_lm kv_init_batched: ggml_init failed"); return false; } @@ -167,8 +168,8 @@ bool kv_init_batched(KvCache & cache, cache.buffer = ggml_backend_alloc_ctx_tensors(cache.ctx, backend); if (cache.buffer == nullptr) { - std::fprintf(stderr, - "causal_lm kv_init_batched: buffer alloc failed\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "causal_lm kv_init_batched: buffer alloc failed"); ggml_free(cache.ctx); cache.ctx = nullptr; return false; @@ -783,8 +784,8 @@ ggml_tensor * block_step_batched( } else { // The manual GQA path is single-shot only; batched decode requires // flash. Callers gate on use_flash and fall back to serial run(). - std::fprintf(stderr, - "causal_lm block_step_batched: non-flash path unsupported\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "causal_lm block_step_batched: non-flash path unsupported"); return nullptr; } @@ -911,8 +912,8 @@ ggml_tensor * block_prefill_batched( scale_attn, 0.0f, 0.0f); o = ggml_reshape_3d(ctx, o, q_dim, T, B); } else { - std::fprintf(stderr, - "causal_lm block_prefill_batched: non-flash unsupported\n"); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "causal_lm block_prefill_batched: non-flash unsupported"); return nullptr; } @@ -956,8 +957,8 @@ bool pack_gate_up(ggml_backend_t backend, if (backend == nullptr || hidden <= 0 || intermediate <= 0 || entries.empty()) { - std::fprintf(stderr, - "%s: pack_gate_up invalid args (hidden=%d intermediate=%d n=%zu)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up invalid args (hidden=%d intermediate=%d n=%zu)", error_tag, hidden, intermediate, entries.size()); return false; } @@ -971,8 +972,8 @@ bool pack_gate_up(ggml_backend_t backend, out_handles.ctx = ggml_init(packed_params); if (out_handles.ctx == nullptr) { - std::fprintf(stderr, - "%s: pack_gate_up ggml_init failed\n", error_tag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up ggml_init failed", error_tag); return false; } @@ -984,14 +985,14 @@ bool pack_gate_up(ggml_backend_t backend, if (e.gate_w == nullptr || e.up_w == nullptr || e.gate_up_w_out == nullptr) { - std::fprintf(stderr, - "%s: pack_gate_up entry %zu has null member\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up entry %zu has null member", error_tag, i); return false; } if (e.gate_w->type != e.up_w->type) { - std::fprintf(stderr, - "%s: pack_gate_up entry %zu gate/up type mismatch (%d vs %d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up entry %zu gate/up type mismatch (%d vs %d)", error_tag, i, static_cast(e.gate_w->type), static_cast(e.up_w->type)); @@ -1000,8 +1001,8 @@ bool pack_gate_up(ggml_backend_t backend, ggml_tensor * t = ggml_new_tensor_2d( out_handles.ctx, e.gate_w->type, hidden, 2 * intermediate); if (t == nullptr) { - std::fprintf(stderr, - "%s: pack_gate_up new_tensor_2d failed at %zu\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up new_tensor_2d failed at %zu", error_tag, i); return false; } @@ -1011,8 +1012,8 @@ bool pack_gate_up(ggml_backend_t backend, out_handles.buffer = ggml_backend_alloc_ctx_tensors( out_handles.ctx, backend); if (out_handles.buffer == nullptr) { - std::fprintf(stderr, - "%s: pack_gate_up backend buffer alloc failed\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up backend buffer alloc failed", error_tag); return false; } @@ -1028,8 +1029,8 @@ bool pack_gate_up(ggml_backend_t backend, const size_t gate_bytes = ggml_nbytes(e.gate_w); const size_t up_bytes = ggml_nbytes(e.up_w); if (ggml_nbytes(gate_up) != gate_bytes + up_bytes) { - std::fprintf(stderr, - "%s: pack_gate_up size mismatch (%zu vs %zu + %zu)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: pack_gate_up size mismatch (%zu vs %zu + %zu)", error_tag, ggml_nbytes(gate_up), gate_bytes, up_bytes); return false; } diff --git a/src/conformer/conformer.cpp b/src/conformer/conformer.cpp index 165bae62..40d0cdda 100644 --- a/src/conformer/conformer.cpp +++ b/src/conformer/conformer.cpp @@ -14,6 +14,7 @@ // shared detect_direct_pw, which is identical across families. #include "conformer/conformer.h" +#include "transcribe-log.h" #include "ggml.h" @@ -1222,9 +1223,9 @@ ggml_tensor * build_pre_encode(ggml_context * ctx, // here as a clear assertion rather than letting mul_mat fail // with a confusing shape mismatch. if (pe.out_w != nullptr && pre_encode_in != pe.out_w->ne[0]) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s encoder: pre_encode_in mismatch: " - "F'*C=%lld but out_w expects %lld\n", + "F'*C=%lld but out_w expects %lld", error_tag != nullptr ? error_tag : "conformer", static_cast(pre_encode_in), static_cast(pe.out_w->ne[0])); diff --git a/src/granite_conformer/shaw_attn.cpp b/src/granite_conformer/shaw_attn.cpp index d7891ecf..9096ff04 100644 --- a/src/granite_conformer/shaw_attn.cpp +++ b/src/granite_conformer/shaw_attn.cpp @@ -4,6 +4,7 @@ // inline this op identically; this file is the single source of truth. #include "shaw_attn.h" +#include "transcribe-log.h" #include "ggml.h" @@ -61,9 +62,9 @@ ggml_tensor * shaw_block_attn(ggml_context * ctx, // error. if (T_pad > T_enc) { if (zero_pad == nullptr) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "granite_conformer::shaw_block_attn: " - "zero_pad is null but T_pad > T_enc\n"); + "zero_pad is null but T_pad > T_enc"); return nullptr; } h = ggml_concat(ctx, h, zero_pad, /*dim=*/1); diff --git a/src/transcribe-bin-loader.cpp b/src/transcribe-bin-loader.cpp index a117b4ef..5375175b 100644 --- a/src/transcribe-bin-loader.cpp +++ b/src/transcribe-bin-loader.cpp @@ -1,6 +1,7 @@ // transcribe-bin-loader.cpp - whisper.cpp `.bin` parser implementation. #include "transcribe-bin-loader.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-backend.h" @@ -139,24 +140,24 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { std::ifstream fin(path, std::ios::binary); if (!fin) { - std::fprintf(stderr, - "%s: failed to open %s\n", kTag, path); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: failed to open %s", kTag, path); return TRANSCRIBE_ERR_GGUF; } // ---- magic ---- uint32_t magic = 0; if (!read_pod(fin, magic)) { - std::fprintf(stderr, "%s: short read on magic\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: short read on magic", kTag); return TRANSCRIBE_ERR_GGUF; } if (magic != k_whisper_bin_magic) { // Caller (transcribe_model_load_file) is responsible for // routing GGUF magic to the GGUF loader before reaching here. // Anything else with non-`ggml` magic is just unsupported. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: bad magic 0x%08x (expected 0x%08x for legacy " - "whisper.cpp .bin)\n", + "whisper.cpp .bin)", kTag, magic, k_whisper_bin_magic); return TRANSCRIBE_ERR_GGUF; } @@ -175,7 +176,7 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { !read_pod(fin, hp.n_mels) || !read_pod(fin, hp.ftype)) { - std::fprintf(stderr, "%s: short read on hparams\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: short read on hparams", kTag); return TRANSCRIBE_ERR_GGUF; } @@ -183,10 +184,10 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { // Magic was `ggml` but the geometry isn't Whisper. Likely a // Silero VAD `.bin` or some other ggml artifact — reject // cleanly so the caller surfaces UNSUPPORTED_ARCH. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: hparams do not match a known Whisper geometry " "(n_audio_layer=%d n_text_layer=%d n_mels=%d " - "n_vocab=%d) — refusing to load as Whisper\n", + "n_vocab=%d) — refusing to load as Whisper", kTag, hp.n_audio_layer, hp.n_text_layer, hp.n_mels, hp.n_vocab); return TRANSCRIBE_ERR_UNSUPPORTED_ARCH; @@ -201,19 +202,19 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { if (!read_pod(fin, out.n_mel_filters) || !read_pod(fin, out.n_fft_filters)) { - std::fprintf(stderr, - "%s: short read on mel filter dims\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read on mel filter dims", kTag); return TRANSCRIBE_ERR_GGUF; } if (out.n_mel_filters != hp.n_mels) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: mel filter n_mel=%d disagrees with hparams " - "n_mels=%d\n", kTag, out.n_mel_filters, hp.n_mels); + "n_mels=%d", kTag, out.n_mel_filters, hp.n_mels); return TRANSCRIBE_ERR_GGUF; } if (out.n_fft_filters <= 0 || out.n_mel_filters <= 0) { - std::fprintf(stderr, - "%s: invalid mel filter dims n_mel=%d n_fft=%d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: invalid mel filter dims n_mel=%d n_fft=%d", kTag, out.n_mel_filters, out.n_fft_filters); return TRANSCRIBE_ERR_GGUF; } @@ -223,10 +224,10 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { // other value here would mean a size-mismatched ggml_backend_tensor_set // later (partial write at best, OOB at worst). if (out.n_fft_filters != 201) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: mel filter n_fft=%d is not 201 (whisper " "canonical n_fft=400 → n_fft/2+1=201); refusing " - "to load\n", kTag, out.n_fft_filters); + "to load", kTag, out.n_fft_filters); return TRANSCRIBE_ERR_GGUF; } const size_t fb_elems = @@ -236,8 +237,8 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { fin.read(reinterpret_cast(out.mel_filterbank.data()), static_cast(fb_elems * sizeof(float))); if (!fin) { - std::fprintf(stderr, - "%s: short read on mel filterbank (%zu floats)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read on mel filterbank (%zu floats)", kTag, fb_elems); return TRANSCRIBE_ERR_GGUF; } @@ -245,15 +246,15 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { // ---- vocab ---- int32_t n_vocab_in_file = 0; if (!read_pod(fin, n_vocab_in_file)) { - std::fprintf(stderr, "%s: short read on vocab count\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: short read on vocab count", kTag); return TRANSCRIBE_ERR_GGUF; } if (n_vocab_in_file <= 0 || n_vocab_in_file > hp.n_vocab + 64) { // The file's vocab count may legitimately be smaller than // hparams.n_vocab (whisper.cpp synthesizes the missing tokens // at load), but it should never overshoot by much. - std::fprintf(stderr, - "%s: implausible vocab count %d (hparams n_vocab=%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: implausible vocab count %d (hparams n_vocab=%d)", kTag, n_vocab_in_file, hp.n_vocab); return TRANSCRIBE_ERR_GGUF; } @@ -262,15 +263,15 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { for (int32_t i = 0; i < n_vocab_in_file; ++i) { uint32_t len = 0; if (!read_pod(fin, len)) { - std::fprintf(stderr, - "%s: short read on vocab token %d length\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read on vocab token %d length", kTag, i); return TRANSCRIBE_ERR_GGUF; } if (len > 1024) { // No legitimate Whisper token is anywhere near this long. - std::fprintf(stderr, - "%s: vocab token %d claims absurd length %u\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: vocab token %d claims absurd length %u", kTag, i, len); return TRANSCRIBE_ERR_GGUF; } @@ -278,8 +279,8 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { if (len > 0) { fin.read(scratch.data(), static_cast(len)); if (!fin) { - std::fprintf(stderr, - "%s: short read on vocab token %d body\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read on vocab token %d body", kTag, i); return TRANSCRIBE_ERR_GGUF; } @@ -301,8 +302,8 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { break; } if (!fin || fin.gcount() != sizeof(n_dims)) { - std::fprintf(stderr, - "%s: truncated tensor header (after %zu tensors)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: truncated tensor header (after %zu tensors)", kTag, out.tensors.size()); return TRANSCRIBE_ERR_GGUF; } @@ -310,26 +311,26 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { int32_t name_len = 0; int32_t ttype = 0; if (!read_pod(fin, name_len) || !read_pod(fin, ttype)) { - std::fprintf(stderr, - "%s: truncated tensor header (after %zu tensors)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: truncated tensor header (after %zu tensors)", kTag, out.tensors.size()); return TRANSCRIBE_ERR_GGUF; } if (n_dims < 1 || n_dims > 4) { - std::fprintf(stderr, - "%s: tensor n_dims=%d out of range\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor n_dims=%d out of range", kTag, n_dims); return TRANSCRIBE_ERR_GGUF; } if (name_len <= 0 || name_len > 256) { - std::fprintf(stderr, - "%s: tensor name_len=%d out of range\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor name_len=%d out of range", kTag, name_len); return TRANSCRIBE_ERR_GGUF; } if (!is_known_ggml_type(ttype)) { - std::fprintf(stderr, - "%s: unknown ttype %d\n", kTag, ttype); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: unknown ttype %d", kTag, ttype); return TRANSCRIBE_ERR_GGUF; } @@ -346,13 +347,13 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { for (int32_t i = 0; i < n_dims; ++i) { int32_t v = 0; if (!read_pod(fin, v)) { - std::fprintf(stderr, - "%s: truncated tensor dims\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: truncated tensor dims", kTag); return TRANSCRIBE_ERR_GGUF; } if (v <= 0) { - std::fprintf(stderr, - "%s: tensor dim ne[%d]=%d non-positive\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor dim ne[%d]=%d non-positive", kTag, i, v); return TRANSCRIBE_ERR_GGUF; } @@ -362,8 +363,8 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { std::string name(static_cast(name_len), '\0'); fin.read(name.data(), name_len); if (!fin) { - std::fprintf(stderr, - "%s: truncated tensor name\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: truncated tensor name", kTag); return TRANSCRIBE_ERR_GGUF; } entry.name = std::move(name); @@ -372,10 +373,10 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { // for the declared (type, ne). entry.nbytes = tensor_nbytes(entry.type, entry.ne); if (entry.nbytes == 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" has 0 nbytes (type=%s, " "ne=[%lld,%lld,%lld,%lld] not a multiple of " - "block size)\n", + "block size)", kTag, entry.name.c_str(), ggml_type_name(entry.type), (long long)entry.ne[0], (long long)entry.ne[1], @@ -388,9 +389,9 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { fin.seekg(static_cast(entry.nbytes), std::ios::cur); if (!fin) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" payload runs past end of file " - "(offset=%llu nbytes=%llu) — file is truncated\n", + "(offset=%llu nbytes=%llu) — file is truncated", kTag, entry.name.c_str(), (unsigned long long)entry.offset, (unsigned long long)entry.nbytes); @@ -401,7 +402,7 @@ transcribe_status parse_whisper_bin(const char * path, WhisperBinModel & out) { } if (out.tensors.empty()) { - std::fprintf(stderr, "%s: file declares no tensors\n", kTag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: file declares no tensors", kTag); return TRANSCRIBE_ERR_GGUF; } @@ -415,8 +416,8 @@ transcribe_status stream_tensor_data_from_bin( { std::ifstream fin(path, std::ios::binary); if (!fin) { - std::fprintf(stderr, - "%s: failed to reopen %s for tensor data\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: failed to reopen %s for tensor data", error_tag, path.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -427,16 +428,16 @@ transcribe_status stream_tensor_data_from_bin( for (const BinStreamSlot & s : slots) { if (s.dst == nullptr) { - std::fprintf(stderr, - "%s: stream slot has null dst tensor\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: stream slot has null dst tensor", error_tag); return TRANSCRIBE_ERR_GGUF; } const size_t want = ggml_nbytes(s.dst); if (s.src_bytes != want) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" byte-size mismatch " - "(.bin says %llu, ggml expects %zu)\n", + "(.bin says %llu, ggml expects %zu)", error_tag, s.dst->name, (unsigned long long)s.src_bytes, want); return TRANSCRIBE_ERR_GGUF; @@ -446,16 +447,16 @@ transcribe_status stream_tensor_data_from_bin( } fin.seekg(static_cast(s.src_off)); if (!fin) { - std::fprintf(stderr, - "%s: seek failed for tensor \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: seek failed for tensor \"%s\"", error_tag, s.dst->name); return TRANSCRIBE_ERR_GGUF; } fin.read(reinterpret_cast(staging.data()), static_cast(want)); if (!fin || static_cast(fin.gcount()) != want) { - std::fprintf(stderr, - "%s: short read for tensor \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read for tensor \"%s\"", error_tag, s.dst->name); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index 3c42aec6..0590942a 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -9,6 +9,7 @@ #include "transcribe-load-common.h" #include "transcribe-backend.h" +#include "transcribe-log.h" #include "ggml.h" #include "ggml-alloc.h" @@ -61,7 +62,7 @@ ggml_backend_t try_init_kind(BackendKind wanted, if (be == nullptr) continue; const BackendKind kind = classify_device(dev); - std::fprintf(stderr, "%s: using %s backend: %s\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using %s backend: %s", error_tag, kind_name(kind), ggml_backend_dev_name(dev)); @@ -88,7 +89,7 @@ void append_accel_backends(std::vector & out, ggml_backend_t be = ggml_backend_dev_init(dev, nullptr); if (be == nullptr) continue; - std::fprintf(stderr, "%s: using accel backend: %s\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using accel backend: %s", error_tag, ggml_backend_dev_name(dev)); out.push_back(be); } @@ -100,8 +101,8 @@ ggml_backend_t init_cpu_backend(const char * error_tag) { ggml_backend_t cpu_be = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr); if (cpu_be == nullptr) { - std::fprintf(stderr, - "%s: failed to initialize CPU backend\n", error_tag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: failed to initialize CPU backend", error_tag); } return cpu_be; } @@ -137,7 +138,7 @@ transcribe_status init_backends(transcribe_backend_request requested, const bool with_accel = (requested == TRANSCRIBE_BACKEND_CPU_ACCEL); ggml_backend_t cpu_be = init_cpu_backend(error_tag); if (cpu_be == nullptr) return TRANSCRIBE_ERR_BACKEND; - std::fprintf(stderr, "%s: using cpu backend (%s)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: using cpu backend (%s)", error_tag, with_accel ? "with accel" : "strict"); out.primary = cpu_be; out.primary_kind = BackendKind::Cpu; @@ -164,8 +165,8 @@ transcribe_status init_backends(transcribe_backend_request requested, BackendKind got_kind = BackendKind::Unknown; ggml_backend_t gpu_be = try_init_kind(wanted, error_tag, got_kind); if (gpu_be == nullptr) { - std::fprintf(stderr, - "%s: %s backend requested but not available\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: %s backend requested but not available", error_tag, kind_name(wanted)); return TRANSCRIBE_ERR_BACKEND; } @@ -226,8 +227,8 @@ transcribe_status init_backends(transcribe_backend_request requested, // Unknown enumerator: reject loudly so callers catch ABI drift // during development. Do not let "everything else" silently map // to AUTO — that hides bugs. - std::fprintf(stderr, - "%s: invalid transcribe_backend_request value %d\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: invalid transcribe_backend_request value %d", error_tag, static_cast(requested)); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -243,8 +244,8 @@ transcribe_status stream_tensor_data(const std::string & path, // #ifdef'ing fseeko vs _fseeki64. std::ifstream fin(path, std::ios::binary); if (!fin) { - std::fprintf(stderr, - "%s: failed to reopen %s for tensor data\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: failed to reopen %s for tensor data", error_tag, path.c_str()); return TRANSCRIBE_ERR_GGUF; } @@ -258,8 +259,8 @@ transcribe_status stream_tensor_data(const std::string & path, { const int64_t idx = gguf_find_tensor(gguf_data, t->name); if (idx < 0) { - std::fprintf(stderr, - "%s: tensor \"%s\" not in gguf data\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor \"%s\" not in gguf data", error_tag, t->name); return TRANSCRIBE_ERR_GGUF; } @@ -271,8 +272,8 @@ transcribe_status stream_tensor_data(const std::string & path, static_cast(toffset); fin.seekg(abs_offset); if (!fin) { - std::fprintf(stderr, - "%s: seek failed for tensor \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: seek failed for tensor \"%s\"", error_tag, t->name); return TRANSCRIBE_ERR_GGUF; } @@ -283,8 +284,8 @@ transcribe_status stream_tensor_data(const std::string & path, fin.read(reinterpret_cast(staging.data()), static_cast(nbytes)); if (!fin) { - std::fprintf(stderr, - "%s: short read for tensor \"%s\" (%zu bytes)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: short read for tensor \"%s\" (%zu bytes)", error_tag, t->name, nbytes); return TRANSCRIBE_ERR_GGUF; } @@ -341,8 +342,8 @@ transcribe_status promote_conv_pw_f16_to_f32_on_cpu( ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, plan.primary); if (buffer == nullptr) { - std::fprintf(stderr, - "%s: conv_pw f32 promotion buffer alloc failed\n", error_tag); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: conv_pw f32 promotion buffer alloc failed", error_tag); ggml_free(ctx); return TRANSCRIBE_ERR_BACKEND; } @@ -351,8 +352,8 @@ transcribe_status promote_conv_pw_f16_to_f32_on_cpu( // Dequantize each F16 tensor into its F32 replacement. const auto * f16_traits = ggml_get_type_traits(GGML_TYPE_F16); if (f16_traits == nullptr || f16_traits->to_float == nullptr) { - std::fprintf(stderr, - "%s: no f16 to_float trait — skipping conv pw promotion\n", + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, + "%s: no f16 to_float trait — skipping conv pw promotion", error_tag); // Partial success: the ctx + buffer are already allocated but // unused. Free them so the caller's outparams stay nullptr, @@ -386,9 +387,9 @@ transcribe_status promote_conv_pw_f16_to_f32_on_cpu( *out_ctx = ctx; *out_buffer = buffer; - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "%s: promoted %zu conv pointwise weights from F16 → F32 " - "for CPU backend\n", error_tag, slots.size()); + "for CPU backend", error_tag, slots.size()); return TRANSCRIBE_OK; } @@ -411,8 +412,8 @@ ReadF32Result read_f32_tensor_checked( // Validate type is F32. const enum ggml_type ttype = gguf_get_tensor_type(gguf_ctx, idx); if (ttype != GGML_TYPE_F32) { - std::fprintf(stderr, - "%s: tensor \"%s\" has type %d, expected F32 (%d)\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor \"%s\" has type %d, expected F32 (%d)", error_tag, tensor_name, static_cast(ttype), static_cast(GGML_TYPE_F32)); @@ -424,9 +425,9 @@ ReadF32Result read_f32_tensor_checked( // Validate alignment: byte count must be a multiple of sizeof(float). if (nbytes == 0 || (nbytes % sizeof(float)) != 0) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" has %zu bytes " - "(not a multiple of %zu)\n", + "(not a multiple of %zu)", error_tag, tensor_name, nbytes, sizeof(float)); return ReadF32Result::BadSize; } @@ -435,8 +436,8 @@ ReadF32Result read_f32_tensor_checked( // Validate expected element count when the caller knows the shape. if (expected_elems > 0 && n_elems != expected_elems) { - std::fprintf(stderr, - "%s: tensor \"%s\" has %zu elements, expected %zu\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: tensor \"%s\" has %zu elements, expected %zu", error_tag, tensor_name, n_elems, expected_elems); return ReadF32Result::BadSize; } @@ -451,8 +452,8 @@ ReadF32Result read_f32_tensor_checked( std::ifstream fin(gguf_path, std::ios::binary); if (!fin) { - std::fprintf(stderr, - "%s: failed to open %s for tensor \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: failed to open %s for tensor \"%s\"", error_tag, gguf_path.c_str(), tensor_name); return ReadF32Result::ReadErr; } @@ -462,8 +463,8 @@ ReadF32Result read_f32_tensor_checked( static_cast(t_off); fin.seekg(abs_offset); if (!fin) { - std::fprintf(stderr, - "%s: seek failed for tensor \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: seek failed for tensor \"%s\"", error_tag, tensor_name); return ReadF32Result::ReadErr; } @@ -473,9 +474,9 @@ ReadF32Result read_f32_tensor_checked( static_cast(nbytes)); if (!fin || static_cast(fin.gcount()) != nbytes) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: short read for tensor \"%s\" " - "(got %zu of %zu bytes)\n", + "(got %zu of %zu bytes)", error_tag, tensor_name, static_cast(fin.gcount()), nbytes); out.clear(); diff --git a/src/transcribe-meta.cpp b/src/transcribe-meta.cpp index 1f064c5b..cd519371 100644 --- a/src/transcribe-meta.cpp +++ b/src/transcribe-meta.cpp @@ -11,6 +11,7 @@ #include "transcribe-meta.h" #include "transcribe-model.h" +#include "transcribe-log.h" #include "gguf.h" @@ -198,8 +199,8 @@ transcribe_status read_required_u32_kv(const gguf_context * gguf, return TRANSCRIBE_OK; case KvResult::Absent: case KvResult::BadType: - std::fprintf(stderr, - "%s: required KV \"%s\" missing or wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: required KV \"%s\" missing or wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -218,8 +219,8 @@ transcribe_status read_required_f32_kv(const gguf_context * gguf, return TRANSCRIBE_OK; case KvResult::Absent: case KvResult::BadType: - std::fprintf(stderr, - "%s: required KV \"%s\" missing or wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: required KV \"%s\" missing or wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -238,8 +239,8 @@ transcribe_status read_required_string_kv(const gguf_context * gguf, return TRANSCRIBE_OK; case KvResult::Absent: case KvResult::BadType: - std::fprintf(stderr, - "%s: required KV \"%s\" missing or wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: required KV \"%s\" missing or wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -261,8 +262,8 @@ transcribe_status read_optional_bool_kv(const gguf_context * gguf, out = tmp; return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, - "%s: optional KV \"%s\" has wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: optional KV \"%s\" has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -284,8 +285,8 @@ transcribe_status read_optional_string_kv(const gguf_context * gguf, out = std::move(v); return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, - "%s: optional KV \"%s\" has wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: optional KV \"%s\" has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } @@ -307,8 +308,8 @@ transcribe_status read_optional_int32_kv(const gguf_context * gguf, out = tmp; return TRANSCRIBE_OK; case KvResult::BadType: - std::fprintf(stderr, - "%s: optional KV \"%s\" has wrong type\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: optional KV \"%s\" has wrong type", error_tag, key); return TRANSCRIBE_ERR_GGUF; } diff --git a/src/transcribe-tokenizer.cpp b/src/transcribe-tokenizer.cpp index ba49ae55..189a9e2d 100644 --- a/src/transcribe-tokenizer.cpp +++ b/src/transcribe-tokenizer.cpp @@ -14,6 +14,7 @@ #include "transcribe-meta.h" #include "transcribe-unicode.h" +#include "transcribe-log.h" #include "gguf.h" @@ -340,9 +341,9 @@ transcribe_status encode_tiktoken_raw_bytes( // Whisper / tiktoken uses the GPT-2 regex; falling back to // raw bytes without a regex would produce a different // tokenization. Reject loudly. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "tokenizer.encode: pretokenizer \"%s\" not " - "supported with raw-bytes vocab\n", + "supported with raw-bytes vocab", pre.c_str()); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -436,9 +437,9 @@ transcribe_status encode_tiktoken_raw_bytes( const std::string sub(piece, j, 1); const auto it2 = piece_to_id.find(sub); if (it2 == piece_to_id.end()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "tokenizer.encode: byte 0x%02X not in " - "vocab\n", + "vocab", static_cast(static_cast(piece[j]))); return TRANSCRIBE_ERR_GGUF; } @@ -475,9 +476,9 @@ transcribe_status Tokenizer::encode(const std::string & text, // under-merged output (every character its own token), which // is never what a caller wants. Surface the configuration // gap instead. - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "tokenizer.encode: \"gpt2\" tokenizer loaded without " - "tokenizer.ggml.merges; encode unavailable\n"); + "tokenizer.ggml.merges; encode unavailable"); return TRANSCRIBE_ERR_GGUF; } if (text.empty()) { @@ -496,9 +497,9 @@ transcribe_status Tokenizer::encode(const std::string & text, words = unicode::pretokenize_granite(text); } else { if (pre_ != "qwen2" && !pre_.empty()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "tokenizer.encode: unknown tokenizer.ggml.pre " - "\"%s\"; falling back to qwen2\n", pre_.c_str()); + "\"%s\"; falling back to qwen2", pre_.c_str()); } words = unicode::pretokenize_qwen2(text); } @@ -614,9 +615,9 @@ transcribe_status Tokenizer::encode(const std::string & text, const std::string sub(piece, j, clen); const auto it2 = piece_to_id_.find(sub); if (it2 == piece_to_id_.end()) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "tokenizer.encode: byte-fallback piece " - "not in vocab (\"%s\")\n", sub.c_str()); + "not in vocab (\"%s\")", sub.c_str()); return TRANSCRIBE_ERR_GGUF; } out_ids.push_back(it2->second); @@ -767,9 +768,9 @@ transcribe_status Tokenizer::load(const gguf_context * gguf) { // llama.cpp's bpe_ranks loader. const size_t sp = line.find(' ', 1); if (sp == std::string::npos) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "tokenizer.load: merge %zu has no space " - "separator: \"%s\"\n", i, line.c_str()); + "separator: \"%s\"", i, line.c_str()); return TRANSCRIBE_ERR_GGUF; } std::string key; diff --git a/src/transcribe-weights-util.cpp b/src/transcribe-weights-util.cpp index f04a56e7..305715b8 100644 --- a/src/transcribe-weights-util.cpp +++ b/src/transcribe-weights-util.cpp @@ -7,6 +7,7 @@ // identical apart from the "parakeet:"/"cohere:" log prefix). #include "transcribe-weights-util.h" +#include "transcribe-log.h" #include "ggml.h" @@ -22,7 +23,7 @@ ggml_tensor * find_tensor(ggml_context * ctx_meta, { ggml_tensor * t = ggml_get_tensor(ctx_meta, name); if (t == nullptr) { - std::fprintf(stderr, "%s: missing tensor \"%s\"\n", error_tag, name); + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: missing tensor \"%s\"", error_tag, name); return nullptr; } @@ -50,17 +51,17 @@ ggml_tensor * find_tensor(ggml_context * ctx_meta, off += static_cast(n); first = false; } - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" type mismatch: " - "expected one of {%s}, got %s\n", + "expected one of {%s}, got %s", error_tag, name, allowed_buf, ggml_type_name(t->type)); return nullptr; } const size_t n_expected = expected_ne.size(); if (n_expected == 0 || n_expected > GGML_MAX_DIMS) { - std::fprintf(stderr, - "%s: bad expected_ne size %zu for \"%s\"\n", + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "%s: bad expected_ne size %zu for \"%s\"", error_tag, n_expected, name); return nullptr; } @@ -68,9 +69,9 @@ ggml_tensor * find_tensor(ggml_context * ctx_meta, auto it = expected_ne.begin(); for (size_t i = 0; i < n_expected; ++i, ++it) { if (t->ne[i] != *it) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" shape mismatch: " - "expected ne[%zu]=%lld, got %lld\n", + "expected ne[%zu]=%lld, got %lld", error_tag, name, i, static_cast(*it), static_cast(t->ne[i])); @@ -82,9 +83,9 @@ ggml_tensor * find_tensor(ggml_context * ctx_meta, // higher-rank tensor with matching leading dims. for (size_t i = n_expected; i < GGML_MAX_DIMS; ++i) { if (t->ne[i] != 1) { - std::fprintf(stderr, + log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: tensor \"%s\" has unexpected non-1 " - "ne[%zu]=%lld (rank too high)\n", + "ne[%zu]=%lld (rank too high)", error_tag, name, i, static_cast(t->ne[i])); return nullptr; diff --git a/src/transcribe.cpp b/src/transcribe.cpp index 7123c31a..42eb2c95 100644 --- a/src/transcribe.cpp +++ b/src/transcribe.cpp @@ -83,6 +83,73 @@ extern "C" const char * transcribe_status_string(int status) { } } +// --------------------------------------------------------------------------- +// Version +// --------------------------------------------------------------------------- + +// TRANSCRIBE_COMMIT is stamped by the build: the top-level CMakeLists.txt +// captures `git rev-parse --short HEAD` at configure time and passes it as a +// compile definition. Fall back to "unknown" for builds without git metadata +// (source tarballs, etc.) so the accessor never returns NULL. TRANSCRIBE_VERSION +// comes from , the single source of truth for the version. +#ifndef TRANSCRIBE_COMMIT +#define TRANSCRIBE_COMMIT "unknown" +#endif + +extern "C" const char * transcribe_version(void) { + return TRANSCRIBE_VERSION; +} + +extern "C" const char * transcribe_version_commit(void) { + return TRANSCRIBE_COMMIT; +} + +// --------------------------------------------------------------------------- +// ABI metadata +// --------------------------------------------------------------------------- +// +// sizeof/alignof for the public structs, so a binding can verify its own +// layout against this build before constructing instances. See the contract in +// . Keep this switch in sync with the transcribe_abi_struct enum. + +extern "C" size_t transcribe_abi_struct_size(transcribe_abi_struct which) { + switch (which) { + case TRANSCRIBE_ABI_MODEL_LOAD_PARAMS: return sizeof(struct transcribe_model_load_params); + case TRANSCRIBE_ABI_SESSION_PARAMS: return sizeof(struct transcribe_session_params); + case TRANSCRIBE_ABI_RUN_PARAMS: return sizeof(struct transcribe_run_params); + case TRANSCRIBE_ABI_STREAM_PARAMS: return sizeof(struct transcribe_stream_params); + case TRANSCRIBE_ABI_CAPABILITIES: return sizeof(struct transcribe_capabilities); + case TRANSCRIBE_ABI_TIMINGS: return sizeof(struct transcribe_timings); + case TRANSCRIBE_ABI_SEGMENT: return sizeof(struct transcribe_segment); + case TRANSCRIBE_ABI_WORD: return sizeof(struct transcribe_word); + case TRANSCRIBE_ABI_TOKEN: return sizeof(struct transcribe_token); + case TRANSCRIBE_ABI_STREAM_UPDATE: return sizeof(struct transcribe_stream_update); + case TRANSCRIBE_ABI_STREAM_TEXT: return sizeof(struct transcribe_stream_text); + case TRANSCRIBE_ABI_SESSION_LIMITS: return sizeof(struct transcribe_session_limits); + case TRANSCRIBE_ABI_EXT: return sizeof(struct transcribe_ext); + } + return 0; // unknown id: "cannot verify", never a real size +} + +extern "C" size_t transcribe_abi_struct_align(transcribe_abi_struct which) { + switch (which) { + case TRANSCRIBE_ABI_MODEL_LOAD_PARAMS: return alignof(struct transcribe_model_load_params); + case TRANSCRIBE_ABI_SESSION_PARAMS: return alignof(struct transcribe_session_params); + case TRANSCRIBE_ABI_RUN_PARAMS: return alignof(struct transcribe_run_params); + case TRANSCRIBE_ABI_STREAM_PARAMS: return alignof(struct transcribe_stream_params); + case TRANSCRIBE_ABI_CAPABILITIES: return alignof(struct transcribe_capabilities); + case TRANSCRIBE_ABI_TIMINGS: return alignof(struct transcribe_timings); + case TRANSCRIBE_ABI_SEGMENT: return alignof(struct transcribe_segment); + case TRANSCRIBE_ABI_WORD: return alignof(struct transcribe_word); + case TRANSCRIBE_ABI_TOKEN: return alignof(struct transcribe_token); + case TRANSCRIBE_ABI_STREAM_UPDATE: return alignof(struct transcribe_stream_update); + case TRANSCRIBE_ABI_STREAM_TEXT: return alignof(struct transcribe_stream_text); + case TRANSCRIBE_ABI_SESSION_LIMITS: return alignof(struct transcribe_session_limits); + case TRANSCRIBE_ABI_EXT: return alignof(struct transcribe_ext); + } + return 0; +} + namespace { // Ordered rank for timestamp granularities, used by the dispatcher to @@ -915,10 +982,10 @@ extern "C" transcribe_status transcribe_model_load_file( // argument" alone doesn't tell the caller which field tripped. When // multi-device support lands this check relaxes to `< 0 || >= n_devices`. if (params->gpu_device != 0) { - std::fprintf(stderr, + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "transcribe_model_load_file: gpu_device must be 0 (auto) in 0.x " "(got %d); multi-device selection is reserved for a " - "future release\n", params->gpu_device); + "future release", params->gpu_device); return TRANSCRIBE_ERR_INVALID_ARG; } @@ -954,9 +1021,9 @@ extern "C" transcribe_status transcribe_model_load_file( } fin.read(reinterpret_cast(&magic), sizeof(magic)); if (!fin || fin.gcount() != sizeof(magic)) { - std::fprintf(stderr, + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "transcribe_model_load_file: short read on file " - "magic for %s\n", path); + "magic for %s", path); return TRANSCRIBE_ERR_GGUF; } } From a032421c9d57fa8cd3f96c1c4f954f9218d42a48 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Jun 2026 08:22:07 +0800 Subject: [PATCH 03/34] python bindings first go --- .github/workflows/python-bindings.yml | 122 ++ CMakeLists.txt | 83 +- CMakePresets.json | 53 + bindings/python/README.md | 43 +- bindings/python/_generate/README.md | 38 + .../python/_generate/check_version_sync.py | 92 ++ bindings/python/_generate/generate.py | 362 ++++++ bindings/python/examples/transcribe_wav.py | 91 ++ bindings/python/pyproject.toml | 22 +- .../python/src/transcribe_cpp/__init__.py | 1064 ++++++++++++++++- bindings/python/src/transcribe_cpp/_abi.py | 77 ++ .../python/src/transcribe_cpp/_generated.py | 374 ++++++ .../python/src/transcribe_cpp/_library.py | 334 ++++++ bindings/python/src/transcribe_cpp/errors.py | 113 ++ bindings/python/src/transcribe_cpp/py.typed | 0 bindings/python/tests/conftest.py | 88 ++ bindings/python/tests/test_abi.py | 94 ++ .../python/tests/test_provider_discovery.py | 168 +++ bindings/python/tests/test_streaming.py | 48 + bindings/python/tests/test_transcribe.py | 66 + bindings/python/uv.lock | 197 +++ include/transcribe.h | 85 ++ src/CMakeLists.txt | 36 +- tests/CMakeLists.txt | 20 + tests/api_smoke.c | 51 + 25 files changed, 3696 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/python-bindings.yml create mode 100644 CMakePresets.json create mode 100644 bindings/python/_generate/README.md create mode 100644 bindings/python/_generate/check_version_sync.py create mode 100644 bindings/python/_generate/generate.py create mode 100644 bindings/python/examples/transcribe_wav.py create mode 100644 bindings/python/src/transcribe_cpp/_abi.py create mode 100644 bindings/python/src/transcribe_cpp/_generated.py create mode 100644 bindings/python/src/transcribe_cpp/_library.py create mode 100644 bindings/python/src/transcribe_cpp/errors.py create mode 100644 bindings/python/src/transcribe_cpp/py.typed create mode 100644 bindings/python/tests/conftest.py create mode 100644 bindings/python/tests/test_abi.py create mode 100644 bindings/python/tests/test_provider_discovery.py create mode 100644 bindings/python/tests/test_streaming.py create mode 100644 bindings/python/tests/test_transcribe.py create mode 100644 bindings/python/uv.lock diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml new file mode 100644 index 00000000..40b8d6ad --- /dev/null +++ b/.github/workflows/python-bindings.yml @@ -0,0 +1,122 @@ +name: python-bindings + +# Locks in what the Python bindings already guarantee (see +# docs/python-bindings-distribution-plan.md, Milestone 1): +# - lint: generated-FFI drift gate + version-sync across the three +# sources of truth (header / pyproject / __init__). +# - cpp-tests: the full C++ white-box suite against a static build. +# - python-shared: a shared libtranscribe, the pure-C api_smoke as an +# exported-symbol canary, and the Python no-model tests +# (import, ABI layout, version gate, status/enum agreement). +# +# Real-model tests are intentionally not run here: they need multi-GB GGUFs CI +# cannot ship. They live behind env vars and skip cleanly. A tiny redistributable +# canary GGUF (plan M1) will later let one real transcription run in CI. + +on: + push: + branches: [main] + paths: + - "bindings/python/**" + - "include/**" + - "src/**" + - "tests/**" + - "ggml/**" + - "CMakeLists.txt" + - ".github/workflows/python-bindings.yml" + pull_request: + paths: + - "bindings/python/**" + - "include/**" + - "src/**" + - "tests/**" + - "ggml/**" + - "CMakeLists.txt" + - ".github/workflows/python-bindings.yml" + workflow_dispatch: + +# The generated FFI layer is pinned to one libclang so the drift gate's embedded +# `libclang: ` line is reproducible across runs. +env: + LIBCLANG_PIN: "libclang==18.1.1" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install clang (libclang resource-dir headers) + run: sudo apt-get update && sudo apt-get install -y clang + - name: Generated-FFI drift gate + run: | + uv run --no-project --with "$LIBCLANG_PIN" \ + bindings/python/_generate/generate.py --check + - name: Version sync (header / pyproject / __init__) + run: uv run --no-project bindings/python/_generate/check_version_sync.py + + cpp-tests: + name: cpp-tests (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Install uv (fixtures are generated via uv) + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install build deps (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build zlib1g-dev + - name: Install build deps (macOS) + if: runner.os == 'macOS' + run: brew install ninja + - name: Configure (static white-box build) + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build -j + - name: Test (full white-box suite) + run: ctest --test-dir build --output-on-failure + + python-shared: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install build deps + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build zlib1g-dev + # Shared build in the official-wheel posture (no OpenMP / system BLAS), so + # the canary exercises the same configuration the provider wheels ship. + - name: Configure (shared, wheel posture) + run: | + cmake -B build-shared -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DTRANSCRIBE_BUILD_SHARED=ON \ + -DTRANSCRIBE_USE_OPENMP=OFF \ + -DTRANSCRIBE_USE_SYSTEM_BLAS=OFF + - name: Build + run: cmake --build build-shared -j + - name: api_smoke (exported-symbol canary) + extension umbrella + run: ctest --test-dir build-shared --output-on-failure + - name: Python no-model tests + run: | + # libtranscribe.so is dlopen'd by ctypes; help it resolve its sibling + # ggml shared libs in the build tree. + export LD_LIBRARY_PATH="$(find "$PWD/build-shared" -name '*.so' \ + -printf '%h\n' | sort -u | tr '\n' ':')$LD_LIBRARY_PATH" + # The loader auto-discovers build-shared/src/libtranscribe.so from the + # repo root; model tests skip (no GGUF in CI). + uv run --project bindings/python --extra test \ + pytest bindings/python/tests -q -rs diff --git a/CMakeLists.txt b/CMakeLists.txt index e8d49efa..09f85cb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,50 @@ cmake_minimum_required(VERSION 3.16) -project(transcribe LANGUAGES C CXX) + +# ----------------------------------------------------------------------------- +# Version (single source of truth: include/transcribe.h) +# ----------------------------------------------------------------------------- +# +# The TRANSCRIBE_VERSION_MAJOR/MINOR/PATCH macros in the public header are the +# one place the library version is defined. Parse them here, before project(), +# so the CMake project version, the shared-library VERSION/SOVERSION, and the +# value transcribe_version() reports all derive from the same source and cannot +# drift. To bump the library version, edit include/transcribe.h. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/include/transcribe.h" _transcribe_header) +string(REGEX MATCH "define +TRANSCRIBE_VERSION_MAJOR +([0-9]+)" _ "${_transcribe_header}") +set(TRANSCRIBE_VERSION_MAJOR "${CMAKE_MATCH_1}") +string(REGEX MATCH "define +TRANSCRIBE_VERSION_MINOR +([0-9]+)" _ "${_transcribe_header}") +set(TRANSCRIBE_VERSION_MINOR "${CMAKE_MATCH_1}") +string(REGEX MATCH "define +TRANSCRIBE_VERSION_PATCH +([0-9]+)" _ "${_transcribe_header}") +set(TRANSCRIBE_VERSION_PATCH "${CMAKE_MATCH_1}") +if(NOT TRANSCRIBE_VERSION_MAJOR MATCHES "^[0-9]+$" OR + NOT TRANSCRIBE_VERSION_MINOR MATCHES "^[0-9]+$" OR + NOT TRANSCRIBE_VERSION_PATCH MATCHES "^[0-9]+$") + message(FATAL_ERROR + "Failed to parse TRANSCRIBE_VERSION_{MAJOR,MINOR,PATCH} from " + "include/transcribe.h") +endif() + +project(transcribe + VERSION ${TRANSCRIBE_VERSION_MAJOR}.${TRANSCRIBE_VERSION_MINOR}.${TRANSCRIBE_VERSION_PATCH} + LANGUAGES C CXX) + +# Short git commit for transcribe_version_commit(), captured at configure time. +# Best-effort: source tarballs and non-git trees fall back to "unknown". This is +# a configure-time snapshot (same posture as ggml's GGML_COMMIT); reconfigure to +# refresh it. +set(TRANSCRIBE_BUILD_COMMIT "unknown") +find_package(Git QUIET) +if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") + execute_process( + COMMAND ${GIT_EXECUTABLE} -C "${CMAKE_CURRENT_SOURCE_DIR}" rev-parse --short HEAD + OUTPUT_VARIABLE _transcribe_git_sha + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(_transcribe_git_sha) + set(TRANSCRIBE_BUILD_COMMIT "${_transcribe_git_sha}") + endif() +endif() +message(STATUS "transcribe version: ${PROJECT_VERSION} (commit ${TRANSCRIBE_BUILD_COMMIT})") # ----------------------------------------------------------------------------- # Standards @@ -42,6 +87,23 @@ option(TRANSCRIBE_CUDA "Enable CUDA backend" OFF) # opt-in; Linu option(TRANSCRIBE_SANITIZE "Enable ASan + UBSan" OFF) option(TRANSCRIBE_LTO "Enable LTO in Release" OFF) +# Library link mode and wheel-hygiene switches (see +# docs/python-bindings-distribution-plan.md). +# +# TRANSCRIBE_BUILD_SHARED OFF (default) builds a static libtranscribe + static +# ggml, so `cmake -B build` yields a self-contained transcribe-cli with no +# libtranscribe/libggml to chase at runtime — the posture every porting, test, +# and bench workflow relies on. The Python wheel/provider build sets it ON to +# produce a shared libtranscribe the FFI layer can dlopen. +# +# TRANSCRIBE_USE_OPENMP / TRANSCRIBE_USE_SYSTEM_BLAS default ON to keep the +# developer build fast (ggml OpenMP, Accelerate/system BLAS host decoder). +# Official provider wheels pass them OFF so no libgomp/libiomp or OpenBLAS/MKL +# runtime is vendored into the wheel where it can collide with PyTorch/NumPy. +option(TRANSCRIBE_BUILD_SHARED "Build libtranscribe + ggml as shared libraries" OFF) +option(TRANSCRIBE_USE_OPENMP "Use OpenMP (ggml + Parakeet host-decoder TU)" ON) +option(TRANSCRIBE_USE_SYSTEM_BLAS "Link non-Apple system BLAS for the host decoder" ON) + # Real-model gated tests. OFF by default because the tests need a # converted Parakeet GGUF on disk (~2.4 GB) and CI can't ship one. # When ON, the test reads TRANSCRIBE_REAL_PARAKEET_GGUF from the @@ -131,15 +193,28 @@ set(GGML_VULKAN ${TRANSCRIBE_VULKAN} CACHE BOOL "" FORCE) set(GGML_CUDA ${TRANSCRIBE_CUDA} CACHE BOOL "" FORCE) set(GGML_BLAS OFF CACHE BOOL "" FORCE) +# OpenMP: ggml defaults GGML_OPENMP ON and probes for it gracefully, so leave +# ggml's own default in place when we want OpenMP. Only force it OFF (matching +# the GGML_OPENMP=OFF posture official wheels use) when OpenMP is switched off, +# so one TRANSCRIBE_USE_OPENMP knob covers ggml and our host-decoder TU. +if(NOT TRANSCRIBE_USE_OPENMP) + set(GGML_OPENMP OFF CACHE BOOL "" FORCE) +endif() + # We don't want ggml's tests / examples leaking into our build tree. They # default to OFF when ggml is added as a subdirectory (GGML_STANDALONE OFF), # but pin them anyway so the contract is explicit. set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -# Build ggml as a static library so the resulting transcribe binary can be -# distributed without chasing libggml.dylib at runtime. -set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +# Library link mode. Default OFF (static) keeps the self-contained CLI/test/ +# bench build with no libtranscribe/libggml to chase at runtime; the Python +# wheel/provider build sets TRANSCRIBE_BUILD_SHARED=ON for a shared +# libtranscribe (+ libggml) the FFI layer can dlopen. Both ggml and +# libtranscribe follow this single switch. Symbol visibility is already hidden +# by default (set above), so a shared lib exports only the TRANSCRIBE_API +# surface. +set(BUILD_SHARED_LIBS ${TRANSCRIBE_BUILD_SHARED} CACHE BOOL "" FORCE) # ggml's CMake declares its own warning flags; let it. add_subdirectory(ggml) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..85b745fe --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,53 @@ +{ + "version": 3, + "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, + "configurePresets": [ + { + "name": "wheel-base", + "hidden": true, + "displayName": "Official wheel posture (shared, no vendored OpenMP/BLAS)", + "description": "Shared libtranscribe for the Python provider wheels. OpenMP and non-Apple system BLAS are off so wheel-repair tools (auditwheel/delocate/delvewheel) never vendor an OpenMP/BLAS runtime that would collide with PyTorch/NumPy/MKL in the same process. GGML_NATIVE off so the binary is not tuned to the build machine.", + "binaryDir": "${sourceDir}/build-wheel", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "TRANSCRIBE_BUILD_SHARED": "ON", + "TRANSCRIBE_USE_OPENMP": "OFF", + "TRANSCRIBE_USE_SYSTEM_BLAS": "OFF", + "TRANSCRIBE_BUILD_TESTS": "OFF", + "TRANSCRIBE_BUILD_EXAMPLES": "OFF", + "GGML_NATIVE": "OFF" + } + }, + { + "name": "wheel-linux-cpu", + "inherits": "wheel-base", + "displayName": "Linux x86_64 conservative CPU wheel", + "description": "The permanent compatibility floor. GGML_NATIVE=OFF does NOT disable the x86 SIMD tiers (they default on), so every tier is turned off explicitly: the resulting binary runs on a baseline x86-64 machine without SIGILL. Performance is the job of the fat-CPU (Strategy B) track, not this wheel.", + "cacheVariables": { + "GGML_SSE42": "OFF", + "GGML_AVX": "OFF", + "GGML_AVX_VNNI": "OFF", + "GGML_AVX2": "OFF", + "GGML_BMI2": "OFF", + "GGML_FMA": "OFF", + "GGML_F16C": "OFF", + "GGML_AVX512": "OFF", + "GGML_AVX512_VBMI": "OFF", + "GGML_AVX512_VNNI": "OFF", + "GGML_AVX512_BF16": "OFF" + } + }, + { + "name": "wheel-macos-metal", + "inherits": "wheel-base", + "displayName": "macOS arm64 Metal wheel", + "description": "Metal ships by default on Apple Silicon, with the shader library embedded so there are no sidecar .metal files to vendor or locate at runtime.", + "cacheVariables": { + "TRANSCRIBE_METAL": "ON", + "GGML_METAL": "ON", + "GGML_METAL_EMBED_LIBRARY": "ON" + } + } + ] +} diff --git a/bindings/python/README.md b/bindings/python/README.md index a25c9f5c..eb87153d 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -3,15 +3,48 @@ Python bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp), a C/C++ speech-to-text library built on ggml. -> **Status: placeholder.** This release reserves the `transcribe-cpp` name on -> PyPI while the first-party bindings are developed. It ships no native code and -> exposes no transcription API yet. Watch the repository for the first -> functional release. +> **Status: in development.** The API below works against a locally built native +> library. Prebuilt wheels (bundled native code, GPU provider packages) are not +> published yet — for now you point the binding at a `libtranscribe` shared +> library you built. Watch the repository for the first wheel release. ```python import transcribe_cpp -print(transcribe_cpp.__version__) +with transcribe_cpp.Model("model.gguf") as model: + with model.session() as session: + result = session.run(pcm_float32_16k_mono) + print(result.text) +``` + +`run()` takes 16 kHz mono float32 PCM (buffer-protocol object or sequence). It +does not decode containers or resample — convert first, e.g. +`ffmpeg -i in.wav -ar 16000 -ac 1 out.wav`. + +## Running from a working tree + +The binding loads the native library at import and verifies its ABI layout and +version before use. Build a shared library and the binding finds it +automatically from the repo, or point `TRANSCRIBE_LIBRARY` at one: + +```bash +cmake -B build-shared -DTRANSCRIBE_BUILD_SHARED=ON +cmake --build build-shared --target transcribe + +cd bindings/python +PYTHONPATH=src uv run --no-project python examples/transcribe_wav.py \ + ../../models/whisper-tiny.en/whisper-tiny.en-Q5_K_M.gguf ../../samples/jfk.wav +``` + +Run the test suite. No-model tests (import, ABI layout, version gate, +status-code/enum agreement) always run; model tests skip when the default +whisper-tiny.en + jfk.wav assets are absent (override with +`TRANSCRIBE_SMOKE_MODEL` / `TRANSCRIBE_SMOKE_AUDIO`): + +```bash +cd bindings/python +TRANSCRIBE_LIBRARY=../../build-shared/src/libtranscribe.dylib \ + uv run --extra test pytest ``` - Import package: `transcribe_cpp` diff --git a/bindings/python/_generate/README.md b/bindings/python/_generate/README.md new file mode 100644 index 00000000..9edd372f --- /dev/null +++ b/bindings/python/_generate/README.md @@ -0,0 +1,38 @@ +# FFI generator + +Generates `src/transcribe_cpp/_generated.py` — the low-level ctypes layer — from +`include/transcribe/extensions.h` using libclang. The generated module is +**committed**; it is never hand-edited. + +## Regenerate + +```bash +cd bindings/python +uv run --no-project --with 'libclang==18.1.1' _generate/generate.py +``` + +Run this whenever the public C headers change. libclang is pinned so the output +is deterministic across machines; the freestanding headers (`stdbool.h`, …) come +from the host compiler's resource dir, discovered via `clang -print-resource-dir` +(macOS: `xcrun`). + +## CI gate + +```bash +uv run --no-project --with 'libclang==18.1.1' _generate/generate.py --check +``` + +Exit non-zero if the committed `_generated.py` is out of date. Because the +generator works from the parsed AST, the check is **semantic**: a comment- or +whitespace-only header edit produces no diff, while any real ABI change (a field, +type, enum value, or function signature) does — and then fails CI until the +binding is regenerated. + +## What it emits + +- ctypes `Structure` for every public struct, field-for-field. +- Enum values as module constants. +- `configure(lib)` — `restype`/`argtypes` for every public function. +- `ABI_STRUCT_IDS` and `STRUCT_LAYOUT` (sizes/aligns/offsets), used by + `_abi.verify_layouts()` to check the layer against itself and the loaded + native library at import. diff --git a/bindings/python/_generate/check_version_sync.py b/bindings/python/_generate/check_version_sync.py new file mode 100644 index 00000000..0863764a --- /dev/null +++ b/bindings/python/_generate/check_version_sync.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Fail if the library version drifts across its three sources of truth. + +The native library version is defined once, in +``include/transcribe.h`` (``TRANSCRIBE_VERSION_{MAJOR,MINOR,PATCH}``); CMake +parses it from there. The Python package repeats it in two more places — +``bindings/python/pyproject.toml`` (``project.version``) and the +``__version__`` in ``bindings/python/src/transcribe_cpp/__init__.py``. The +import-time gate enforces base-version match against the *loaded* library at +runtime; this script is the static, build-time counterpart so a forgotten bump +fails CI before anything is published. + +Comparison is on the PEP 440 *release segment* (``MAJOR.MINOR.PATCH``): the +header is always a clean triple, while the Python side may legitimately carry a +``.postN`` packaging suffix that must still be accepted. + + uv run --no-project bindings/python/_generate/check_version_sync.py + +Exit 0 when all three agree on the base version; 1 on drift; 2 if a version +could not be located (treated as a hard error, not a pass). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[3] +HEADER = REPO / "include" / "transcribe.h" +PYPROJECT = REPO / "bindings" / "python" / "pyproject.toml" +INIT = REPO / "bindings" / "python" / "src" / "transcribe_cpp" / "__init__.py" + + +def base_version(version: str) -> str: + """The leading dotted-numeric release segment (suffix stripped).""" + m = re.match(r"\d+(?:\.\d+)*", version.strip()) + return m.group(0) if m else version.strip() + + +def header_version(text: str) -> str | None: + parts = [] + for component in ("MAJOR", "MINOR", "PATCH"): + m = re.search(rf"define\s+TRANSCRIBE_VERSION_{component}\s+(\d+)", text) + if not m: + return None + parts.append(m.group(1)) + return ".".join(parts) + + +def pyproject_version(text: str) -> str | None: + # project.version is a top-level string in [project]; match it directly + # rather than pulling in a TOML parser (tomllib is 3.11+). + m = re.search(r'(?m)^\s*version\s*=\s*"([^"]+)"', text) + return m.group(1) if m else None + + +def init_version(text: str) -> str | None: + m = re.search(r'(?m)^__version__\s*=\s*"([^"]+)"', text) + return m.group(1) if m else None + + +def main() -> int: + sources = { + "include/transcribe.h": header_version(HEADER.read_text()), + "pyproject.toml": pyproject_version(PYPROJECT.read_text()), + "__init__.__version__": init_version(INIT.read_text()), + } + + missing = [name for name, v in sources.items() if v is None] + if missing: + for name in missing: + print(f"error: could not locate the version in {name}", file=sys.stderr) + return 2 + + bases = {name: base_version(v) for name, v in sources.items()} # type: ignore[arg-type] + distinct = set(bases.values()) + if len(distinct) != 1: + print("version drift across sources (base MAJOR.MINOR.PATCH must agree):", + file=sys.stderr) + for name, v in sources.items(): + print(f" {name}: {v} (base {bases[name]})", file=sys.stderr) + return 1 + + base = distinct.pop() + detail = ", ".join(f"{name}={v}" for name, v in sources.items()) + print(f"version sync ok: base {base} ({detail})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bindings/python/_generate/generate.py b/bindings/python/_generate/generate.py new file mode 100644 index 00000000..5cbe09b0 --- /dev/null +++ b/bindings/python/_generate/generate.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +"""Generate the low-level ctypes FFI layer from the public C headers. + +This parses ``include/transcribe/extensions.h`` with libclang and emits +``src/transcribe_cpp/_generated.py`` — ctypes Structures (field-for-field from +the C structs, with the offsets the C compiler computed), enum constants, +function prototypes, and per-struct ABI metadata. The generated module is +committed; CI regenerates and runs ``git diff --exit-code`` so a header change +that is not reflected in the binding fails the build. + +Because libclang works from the parsed AST (not header text), the output is +semantic: a comment-only or whitespace header edit produces no diff, while any +real ABI change (a field, a type, an enum value, a signature) does. + +Usage: + uv run --no-project --with 'libclang==18.1.1' _generate/generate.py # write + uv run --no-project --with 'libclang==18.1.1' _generate/generate.py --check # CI gate +""" + +from __future__ import annotations + +import argparse +import hashlib +import subprocess +import sys +from pathlib import Path + +import clang.cindex as ci +from clang.cindex import CursorKind, TypeKind + +REPO = Path(__file__).resolve().parents[3] +INCLUDE = REPO / "include" +HEADER = INCLUDE / "transcribe" / "extensions.h" +OUTPUT = REPO / "bindings" / "python" / "src" / "transcribe_cpp" / "_generated.py" + +# Fixed-width / size typedefs map to fixed-width ctypes so the generated layout +# is correct on every target (never c_long, whose width differs LP64 vs LLP64). +_SPELLING_INT = { + "int8_t": "c_int8", "uint8_t": "c_uint8", + "int16_t": "c_int16", "uint16_t": "c_uint16", + "int32_t": "c_int32", "uint32_t": "c_uint32", + "int64_t": "c_int64", "uint64_t": "c_uint64", + "size_t": "c_size_t", "ssize_t": "c_ssize_t", + "intptr_t": "c_ssize_t", "uintptr_t": "c_size_t", +} +_KIND_SCALAR = { + TypeKind.VOID: None, + TypeKind.BOOL: "c_bool", + TypeKind.CHAR_S: "c_char", TypeKind.SCHAR: "c_char", + TypeKind.CHAR_U: "c_ubyte", TypeKind.UCHAR: "c_ubyte", + TypeKind.SHORT: "c_short", TypeKind.USHORT: "c_ushort", + TypeKind.INT: "c_int", TypeKind.UINT: "c_uint", + TypeKind.LONG: "c_long", TypeKind.ULONG: "c_ulong", + TypeKind.LONGLONG: "c_longlong", TypeKind.ULONGLONG: "c_ulonglong", + TypeKind.FLOAT: "c_float", TypeKind.DOUBLE: "c_double", +} + + +def clang_args() -> list[str]: + args = ["-x", "c", "-std=c11", f"-I{INCLUDE}"] + # Freestanding headers (stdbool/stdint/stddef) live in the compiler's + # resource dir; the bundled libclang does not ship them. + try: + resdir = subprocess.check_output( + ["xcrun", "clang", "-print-resource-dir"], text=True, stderr=subprocess.DEVNULL + ).strip() + args.append(f"-isystem{resdir}/include") + sdk = subprocess.check_output( + ["xcrun", "--show-sdk-path"], text=True, stderr=subprocess.DEVNULL + ).strip() + args.append(f"-isysroot{sdk}") + except Exception: + try: + resdir = subprocess.check_output( + ["clang", "-print-resource-dir"], text=True + ).strip() + args.append(f"-isystem{resdir}/include") + except Exception: + pass + return args + + +def in_headers(cursor) -> bool: + f = cursor.location.file + return f is not None and str(INCLUDE) in str(f.name) + + +def int_macro_value(value_tokens): + """Parse an object-like macro's value as an int, or None if it isn't one. + + Captures integer constants (EXT kind FourCCs, version components); skips + string, attribute, and float/expression macros, which don't parse as int. + """ + s = "".join(value_tokens).rstrip("uUlL") + try: + return int(s, 0) + except ValueError: + return None + + +class Surface: + def __init__(self): + self.enum_constants: list[tuple[str, int]] = [] + self.macros: list[tuple[str, int]] = [] # name -> int value + self.structs: dict = {} # name -> cursor + self.struct_order: list[str] = [] + self.functions: dict = {} # name -> cursor + self.abi_ids: dict = {} # struct name -> transcribe_abi_struct id + + +def collect(tu) -> Surface: + s = Surface() + seen_enum = set() + seen_macro = set() + for c in tu.cursor.walk_preorder(): + if not in_headers(c): + continue + if c.kind == CursorKind.ENUM_DECL and c.is_definition(): + for e in c.get_children(): + if e.kind == CursorKind.ENUM_CONSTANT_DECL and e.spelling not in seen_enum: + seen_enum.add(e.spelling) + s.enum_constants.append((e.spelling, e.enum_value)) + elif c.kind == CursorKind.MACRO_DEFINITION and c.spelling.startswith("TRANSCRIBE_"): + if c.spelling in seen_macro: + continue + tokens = [t.spelling for t in c.get_tokens()] + if len(tokens) < 2: + continue # bare guard macro, no value + value = int_macro_value(tokens[1:]) + if value is not None: + seen_macro.add(c.spelling) + s.macros.append((c.spelling, value)) + elif c.kind == CursorKind.STRUCT_DECL and c.is_definition() and c.spelling: + if c.spelling not in s.structs: + s.structs[c.spelling] = c + s.struct_order.append(c.spelling) + elif c.kind == CursorKind.FUNCTION_DECL and c.spelling not in s.functions: + s.functions[c.spelling] = c + + # Map transcribe_abi_struct enum members to struct names: the member + # TRANSCRIBE_ABI_RUN_PARAMS corresponds to struct transcribe_run_params. + for name, value in s.enum_constants: + if name.startswith("TRANSCRIBE_ABI_"): + struct = "transcribe_" + name[len("TRANSCRIBE_ABI_"):].lower() + if struct in s.structs: + s.abi_ids[struct] = value + s.macros.sort(key=lambda kv: kv[0]) # name order for deterministic output + return s + + +def map_type(t, structs: dict) -> str: + """Return a ctypes expression (``_c.``-prefixed) for a clang Type.""" + spelling = t.spelling.replace("const ", "").replace("volatile ", "").strip() + k = t.kind + + if k == TypeKind.POINTER: + pointee = t.get_pointee() + cp = pointee.get_canonical() + if cp.kind == TypeKind.FUNCTIONPROTO: + ret = map_type(cp.get_result(), structs) or "None" + args = [map_type(a, structs) for a in cp.argument_types()] + return f"_c.CFUNCTYPE({', '.join([ret] + args)})" + if cp.kind in (TypeKind.CHAR_S, TypeKind.CHAR_U, TypeKind.SCHAR, TypeKind.UCHAR): + return "_c.c_char_p" + if cp.kind == TypeKind.VOID: + return "_c.c_void_p" + if cp.kind == TypeKind.RECORD: + name = cp.get_declaration().spelling + return f"_c.POINTER({name})" if name in structs else "_c.c_void_p" + inner = map_type(pointee, structs) + return f"_c.POINTER({inner})" if inner and inner != "None" else "_c.c_void_p" + + if spelling in _SPELLING_INT: + return "_c." + _SPELLING_INT[spelling] + if k == TypeKind.ENUM: + return "_c.c_int" + if k in (TypeKind.TYPEDEF, TypeKind.ELABORATED): + return map_type(t.get_canonical(), structs) + if k == TypeKind.RECORD: + return t.get_declaration().spelling + if k == TypeKind.CONSTANTARRAY: + return f"({map_type(t.element_type, structs)} * {t.element_count})" + if k in _KIND_SCALAR: + v = _KIND_SCALAR[k] + return "_c." + v if v else "None" + raise SystemExit(f"unmapped type: {t.spelling!r} kind={k}") + + +def struct_fields(cursor): + return [f for f in cursor.get_children() if f.kind == CursorKind.FIELD_DECL] + + +def order_by_value_deps(structs: dict, order: list[str]) -> list[str]: + """Topologically order structs so a by-value member is defined first.""" + deps = {} + for name in order: + d = set() + for f in struct_fields(structs[name]): + ft = f.type.get_canonical() + if ft.kind == TypeKind.RECORD: + dep = ft.get_declaration().spelling + if dep in structs: + d.add(dep) + elif ft.kind == TypeKind.CONSTANTARRAY: + et = ft.element_type.get_canonical() + if et.kind == TypeKind.RECORD and et.get_declaration().spelling in structs: + d.add(et.get_declaration().spelling) + deps[name] = d + + out, placed = [], set() + while len(out) < len(order): + progress = False + for name in order: + if name in placed: + continue + if deps[name] <= placed: + out.append(name) + placed.add(name) + progress = True + if not progress: # cycle (shouldn't happen for this API) + raise SystemExit(f"by-value struct cycle among {set(order) - placed}") + return out + + +def render(s: Surface, libclang_version: str) -> str: + # The structural payload (everything below the imports) is built first so a + # stable digest can be hashed over it and emitted as PUBLIC_HEADER_HASH. That + # digest is the coarse cross-language ABI tag a native provider echoes back + # at load time: it changes for any real ABI change (a field, type, enum + # value, signature, layout) and stays put for a comment-only header edit, + # exactly like the generator's own drift gate. It does NOT cover the libclang + # version line (that lives in the docstring, above the hashed body). + body: list[str] = [] + b = body.append + + b("# === enum constants ===") + for name, value in s.enum_constants: + b(f"{name} = {value}") + b("") + + b("# === macro constants (integer object-like macros) ===") + for name, value in s.macros: + b(f"{name} = {value}") + b("") + + struct_names = set(s.structs) + b("# === structs ===") + for name in s.struct_order: + b(f"class {name}(_c.Structure):") + b(" pass") + b("") + + layout = {} + for name in order_by_value_deps(s.structs, s.struct_order): + cursor = s.structs[name] + fields, offsets = [], {} + for f in struct_fields(cursor): + fields.append((f.spelling, map_type(f.type, struct_names))) + offsets[f.spelling] = cursor.type.get_offset(f.spelling) // 8 + joined = ", ".join(f'("{fn}", {ct})' for fn, ct in fields) + b(f"{name}._fields_ = [{joined}]") + layout[name] = { + "size": cursor.type.get_size(), + "align": cursor.type.get_align(), + "offsets": offsets, + } + b("") + + b("# === ABI metadata ===") + b("# transcribe_abi_struct id per struct (for the native size/align check).") + b("ABI_STRUCT_IDS = {") + for name in s.struct_order: + if name in s.abi_ids: + b(f" {name!r}: {s.abi_ids[name]},") + b("}") + b("") + b("# C-compiler layout captured at generation (for offset self-check).") + b("STRUCT_LAYOUT = {") + for name in s.struct_order: + lo = layout[name] + b(f" {name!r}: {{'size': {lo['size']}, 'align': {lo['align']}, " + f"'offsets': {lo['offsets']!r}}},") + b("}") + b("") + + b("# === function prototypes ===") + b("def configure(lib):") + b(' """Stamp restype/argtypes onto a loaded CDLL."""') + for name in sorted(s.functions): + fn = s.functions[name] + restype = map_type(fn.result_type, struct_names) + argtypes = [map_type(a.type, struct_names) for a in fn.get_arguments()] + b(f" lib.{name}.restype = {restype}") + b(f" lib.{name}.argtypes = [{', '.join(argtypes)}]") + b("") + + digest = hashlib.sha256("\n".join(body).encode("utf-8")).hexdigest()[:16] + + out = [] + w = out.append + w('"""Low-level ctypes FFI for transcribe.cpp — AUTOGENERATED. DO NOT EDIT.') + w("") + w("Regenerate with:") + w(" uv run --no-project --with 'libclang==18.1.1' \\") + w(" bindings/python/_generate/generate.py") + w("") + w("Source: include/transcribe/extensions.h") + w(f"libclang: {libclang_version}") + w('"""') + w("") + w("import ctypes as _c") + w("") + w("# Stable digest of the ABI surface below (structs, enums, macros, layout,") + w("# prototypes). A native provider package echoes this back so the API") + w("# package can reject an ABI-mismatched provider before dlopen.") + w(f'PUBLIC_HEADER_HASH = "{digest}"') + w("") + out.extend(body) + return "\n".join(out) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--check", action="store_true", + help="exit non-zero if the committed file is out of date") + args = ap.parse_args() + + try: + libclang_version = __import__("importlib.metadata", fromlist=["version"]).version( + "libclang" + ) + except Exception: + libclang_version = "unknown" + + tu = ci.Index.create().parse( + str(HEADER), args=clang_args(), + options=ci.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD, + ) + errors = [d for d in tu.diagnostics if d.severity >= ci.Diagnostic.Error] + if errors: + for d in errors: + print(f"clang error: {d.spelling} at {d.location}", file=sys.stderr) + return 2 + + text = render(collect(tu), libclang_version) + + if args.check: + current = OUTPUT.read_text() if OUTPUT.exists() else "" + if current != text: + print(f"{OUTPUT} is out of date — regenerate with " + "_generate/generate.py", file=sys.stderr) + return 1 + print(f"{OUTPUT.name} is up to date") + return 0 + + OUTPUT.write_text(text) + print(f"wrote {OUTPUT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bindings/python/examples/transcribe_wav.py b/bindings/python/examples/transcribe_wav.py new file mode 100644 index 00000000..7c881ed0 --- /dev/null +++ b/bindings/python/examples/transcribe_wav.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Transcribe a 16 kHz mono WAV with the transcribe_cpp bindings. + + uv run examples/transcribe_wav.py [--timestamps segment] + +WAV decoding here uses only the stdlib ``wave`` module — the binding itself +takes float32 PCM and does not decode containers or resample. Point +TRANSCRIBE_LIBRARY at a libtranscribe shared library, or run from a working tree +with a shared build (cmake -DTRANSCRIBE_BUILD_SHARED=ON). +""" + +from __future__ import annotations + +import argparse +import array +import sys +import wave + +import transcribe_cpp + + +def load_wav_mono16k(path: str) -> array.array: + with wave.open(path, "rb") as w: + n_channels = w.getnchannels() + sample_width = w.getsampwidth() + framerate = w.getframerate() + frames = w.readframes(w.getnframes()) + + if sample_width != 2: + raise SystemExit(f"{path}: expected 16-bit PCM, got {sample_width * 8}-bit") + if framerate != 16000: + raise SystemExit( + f"{path}: expected 16 kHz, got {framerate} Hz — resample first, e.g. " + f"ffmpeg -i in.wav -ar 16000 -ac 1 out.wav" + ) + + pcm16 = array.array("h") + pcm16.frombytes(frames) + if sys.byteorder == "big": + pcm16.byteswap() + + if n_channels > 1: # downmix to mono + mono = array.array("h", [0]) * (len(pcm16) // n_channels) + for i in range(len(mono)): + acc = sum(pcm16[i * n_channels + c] for c in range(n_channels)) + mono[i] = int(acc / n_channels) + pcm16 = mono + + return array.array("f", (s / 32768.0 for s in pcm16)) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("model") + ap.add_argument("audio") + ap.add_argument("--backend", default="auto") + ap.add_argument("--timestamps", default="none", + choices=["none", "auto", "segment", "word", "token"]) + ap.add_argument("--language", default=None) + args = ap.parse_args() + + print(f"transcribe_cpp {transcribe_cpp.__version__} | native " + f"{transcribe_cpp.native_version()} ({transcribe_cpp.native_commit()})") + print(f"library: {transcribe_cpp.library_path()}") + + pcm = load_wav_mono16k(args.audio) + + with transcribe_cpp.Model(args.model, backend=args.backend) as model: + caps = model.capabilities + print(f"model: {model.arch}/{model.variant} on {model.backend} | " + f"max_ts={caps.max_timestamp_kind} translate={caps.supports_translate}") + with model.session() as session: + result = session.run( + pcm, timestamps=args.timestamps, language=args.language + ) + + print(f"\nlanguage: {result.language or '(n/a)'} | " + f"timestamps: {result.timestamp_kind} | " + f"encode {result.timings.encode_ms:.0f} ms") + print(f"\n{result.text.strip()}\n") + + if result.segments and result.timestamp_kind != "none": + for seg in result.segments: + print(f" [{seg.t0_ms / 1000:6.2f} -> {seg.t1_ms / 1000:6.2f}] " + f"{seg.text.strip()}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index a2c73c37..6c3e6f89 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,10 +4,12 @@ build-backend = "hatchling.build" [project] name = "transcribe-cpp" -version = "0.0.0" -description = "Python bindings for transcribe.cpp (pre-release name reservation placeholder)" +version = "0.0.1" +description = "Python bindings for transcribe.cpp" readme = "README.md" -requires-python = ">=3.8" +# 3.8 is EOL (2024-10); 3.9 is the floor. The binding is ctypes-only, so there +# is no per-version compiled extension — one pure-Python package spans the range. +requires-python = ">=3.9" license = "MIT" license-files = ["LICENSE"] authors = [{ name = "The transcribe.cpp authors" }] @@ -17,10 +19,19 @@ classifiers = [ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Multimedia :: Sound/Audio :: Speech", "Topic :: Scientific/Engineering :: Artificial Intelligence", ] +[project.optional-dependencies] +# Test-only deps. Run with: uv run --extra test pytest (from bindings/python). +test = ["pytest>=7"] + [project.urls] Homepage = "https://github.com/handy-computer/transcribe.cpp" Repository = "https://github.com/handy-computer/transcribe.cpp" @@ -28,3 +39,8 @@ Issues = "https://github.com/handy-computer/transcribe.cpp/issues" [tool.hatch.build.targets.wheel] packages = ["src/transcribe_cpp"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +# 77 is the historical CTest "skipped" code; pytest uses skip markers instead. +addopts = "-ra" diff --git a/bindings/python/src/transcribe_cpp/__init__.py b/bindings/python/src/transcribe_cpp/__init__.py index f60d7554..a5876c25 100644 --- a/bindings/python/src/transcribe_cpp/__init__.py +++ b/bindings/python/src/transcribe_cpp/__init__.py @@ -1,12 +1,1062 @@ -"""Python bindings for transcribe.cpp. +"""Python bindings for transcribe.cpp — a ggml speech-to-text library. -This is a placeholder distribution that reserves the ``transcribe-cpp`` name on -PyPI while first-party bindings are under development. It intentionally ships no -native code yet, so importing it succeeds but no transcription API is available. + import transcribe_cpp -See https://github.com/handy-computer/transcribe.cpp for status. + with transcribe_cpp.Model("model.gguf") as model: + with model.session() as session: + result = session.run(pcm_float32_16k_mono) + print(result.text) + +The native library is loaded at import time; its ABI layout and version are +verified against this binding before any model is touched (see +``_abi.verify_layouts`` and the version check below). PCM passed to ``run`` must +be 16 kHz mono float32; resample external audio first, e.g.:: + + ffmpeg -i in.wav -ar 16000 -ac 1 -f f32le out.f32 + +Long-running native calls (model load, run) release the GIL — ctypes does this +for every foreign call — so other Python threads make progress during inference. """ -__version__ = "0.0.0" +from __future__ import annotations + +import ctypes +import os +import threading +from dataclasses import dataclass +from typing import Literal, Sequence, Union + +from . import _abi, _generated +from ._library import _base_version, load_library, selected_provider +from .errors import ( + AbiError, + BackendError, + InputTooLong, + InvalidArgument, + ModelFileNotFound, + ModelLoadError, + NotImplementedByModel, + OutOfMemory, + OutputTruncated, + TranscribeError, + UnsupportedRequest, + raise_for_status, +) + +__version__ = "0.0.1" + +# String-enum types, exported so callers (and type checkers) can name them. +Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda"] +KVType = Literal["auto", "f32", "f16"] +Task = Literal["transcribe", "translate"] +Timestamps = Literal["none", "auto", "segment", "word", "token"] +CommitPolicy = Literal["auto", "on_finalize", "stable_prefix"] +Feature = Literal[ + "initial_prompt", "temperature_fallback", "long_form", + "cancellation", "pnc", "itn", +] + +__all__ = [ + "__version__", + "transcribe", + "Model", + "Session", + "Result", + "Segment", + "Word", + "Token", + "Capabilities", + "Timings", + "Stream", + "StreamUpdate", + "StreamText", + "FamilyExtension", + "WhisperRunOptions", + "MoonshineStreamingOptions", + "ParakeetStreamOptions", + "ParakeetBufferedStreamOptions", + "VoxtralRealtimeStreamOptions", + "Backend", + "KVType", + "Task", + "Timestamps", + "CommitPolicy", + "Feature", + "TranscribeError", + "InvalidArgument", + "ModelFileNotFound", + "ModelLoadError", + "NotImplementedByModel", + "OutOfMemory", + "BackendError", + "UnsupportedRequest", + "AbiError", + "InputTooLong", + "OutputTruncated", + "native_version", + "native_commit", + "library_path", + "native_provider", + "set_log_callback", +] + +# --- native library bootstrap -------------------------------------------- + +_lib, _lib_path = load_library() +_generated.configure(_lib) +_abi.verify_layouts(_lib) + +# _base_version is single-sourced in _library (re-exported here so the import +# gate and tests can reach it as transcribe_cpp._base_version). Pre-1.0 the +# Python package and native library must agree on the base (MAJOR.MINOR.PATCH) +# release segment exactly; a packaging-only fix (0.0.1.post1) keeps the same +# base and so still loads against the 0.0.1 native library. +_native_version = _lib.transcribe_version().decode("ascii") +if _base_version(_native_version) != _base_version(__version__): + raise TranscribeError( + f"transcribe_cpp {__version__} cannot use native library " + f"{_native_version}: pre-1.0 requires a matching base " + f"(MAJOR.MINOR.PATCH) version. Rebuild the native library at the " + "matching version or install a matching native provider." + ) + +_byref = ctypes.byref + +# Callback function types — must match the generated argtypes for +# transcribe_log_set / transcribe_set_abort_callback. ctypes acquires the GIL +# when C invokes these, and a CFUNCTYPE instance must be kept alive for as long +# as C may call it (a module global for logging; on the Session for abort). +_LOG_CFUNC = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p) +_ABORT_CFUNC = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p) + +# Struct aliases onto the generated ctypes layer (the low-level names mirror the +# C tags; alias them for readability here). +_ModelLoadParams = _generated.transcribe_model_load_params +_SessionParams = _generated.transcribe_session_params +_RunParams = _generated.transcribe_run_params +_Capabilities = _generated.transcribe_capabilities +_Timings = _generated.transcribe_timings +_Segment = _generated.transcribe_segment +_Word = _generated.transcribe_word +_Token = _generated.transcribe_token +_StreamParams = _generated.transcribe_stream_params +_StreamUpdate = _generated.transcribe_stream_update +_StreamText = _generated.transcribe_stream_text + +# --- enum maps (values sourced from the generated enum constants) --------- + +_BACKENDS = { + "auto": _generated.TRANSCRIBE_BACKEND_AUTO, + "cpu": _generated.TRANSCRIBE_BACKEND_CPU, + "metal": _generated.TRANSCRIBE_BACKEND_METAL, + "vulkan": _generated.TRANSCRIBE_BACKEND_VULKAN, + "cpu_accel": _generated.TRANSCRIBE_BACKEND_CPU_ACCEL, + "cuda": _generated.TRANSCRIBE_BACKEND_CUDA, +} +_KV_TYPES = { + "auto": _generated.TRANSCRIBE_KV_TYPE_AUTO, + "f32": _generated.TRANSCRIBE_KV_TYPE_F32, + "f16": _generated.TRANSCRIBE_KV_TYPE_F16, +} +_TASKS = { + "transcribe": _generated.TRANSCRIBE_TASK_TRANSCRIBE, + "translate": _generated.TRANSCRIBE_TASK_TRANSLATE, +} +_TIMESTAMPS = { + "none": _generated.TRANSCRIBE_TIMESTAMPS_NONE, + "auto": _generated.TRANSCRIBE_TIMESTAMPS_AUTO, + "segment": _generated.TRANSCRIBE_TIMESTAMPS_SEGMENT, + "word": _generated.TRANSCRIBE_TIMESTAMPS_WORD, + "token": _generated.TRANSCRIBE_TIMESTAMPS_TOKEN, +} +_TIMESTAMP_NAMES = {v: k for k, v in _TIMESTAMPS.items()} +_COMMIT_POLICIES = { + "auto": _generated.TRANSCRIBE_STREAM_COMMIT_AUTO, + "on_finalize": _generated.TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE, + "stable_prefix": _generated.TRANSCRIBE_STREAM_COMMIT_STABLE_PREFIX, +} +_STREAM_STATES = { + _generated.TRANSCRIBE_STREAM_IDLE: "idle", + _generated.TRANSCRIBE_STREAM_ACTIVE: "active", + _generated.TRANSCRIBE_STREAM_FINISHED: "finished", + _generated.TRANSCRIBE_STREAM_FAILED: "failed", +} +_EXT_SLOTS = { + "run": _generated.TRANSCRIBE_EXT_SLOT_RUN, + "stream": _generated.TRANSCRIBE_EXT_SLOT_STREAM, +} +_FEATURES = { + "initial_prompt": _generated.TRANSCRIBE_FEATURE_INITIAL_PROMPT, + "temperature_fallback": _generated.TRANSCRIBE_FEATURE_TEMPERATURE_FALLBACK, + "long_form": _generated.TRANSCRIBE_FEATURE_LONG_FORM, + "cancellation": _generated.TRANSCRIBE_FEATURE_CANCELLATION, + "pnc": _generated.TRANSCRIBE_FEATURE_PNC, + "itn": _generated.TRANSCRIBE_FEATURE_ITN, +} + + +def native_version() -> str: + """Version string of the loaded native library, e.g. ``"0.0.1"``.""" + return _native_version + + +def native_commit() -> str: + """Git commit the native library was built from, or ``"unknown"``.""" + return _lib.transcribe_version_commit().decode("ascii") + + +def library_path() -> str: + """Filesystem path of the loaded native library.""" + return str(_lib_path) + + +def native_provider() -> "Union[str, None]": + """Name of the installed provider package the native library was loaded + from, or None for a dev-tree / ``TRANSCRIBE_LIBRARY`` load.""" + return selected_provider() + + +_log_handler = None +_log_trampoline = None + + +def _log_thunk(level, msg, _userdata): + handler = _log_handler + if handler is not None: + try: + handler(level, msg.decode("utf-8", "replace") if msg else "") + except Exception: + pass # a handler error must never propagate back into C + + +def set_log_callback(handler) -> None: + """Route native log messages to ``handler(level: int, message: str)``; pass + None to silence. + + Install ONCE at startup, before loading models or creating threads (the 0.x + contract for transcribe_log_set). The handler may be invoked from ggml worker + threads, so it must be thread-safe — route to the ``logging`` module or a + queue rather than doing heavy work inline. Levels mirror + TRANSCRIBE_LOG_LEVEL_* (1=info, 2=warn, 3=error, 4=debug).""" + global _log_handler, _log_trampoline + _log_handler = handler + if _log_trampoline is None: + # Install the trampoline once; later calls just swap the Python handler + # behind it, so transcribe_log_set is never re-invoked after threads run. + _log_trampoline = _LOG_CFUNC(_log_thunk) + _lib.transcribe_log_set(_log_trampoline, None) + + +def _status_string(status: int) -> str: + return _lib.transcribe_status_string(status).decode("utf-8", "replace") + + +def _check(status: int, context: str = "") -> None: + raise_for_status(status, _status_string(status), context) + + +def _decode(value) -> str: + return value.decode("utf-8", "replace") if value else "" + + +def _enum(mapping: dict, key: str, what: str) -> int: + try: + return mapping[key] + except KeyError: + raise InvalidArgument( + f"unknown {what} {key!r}; expected one of {sorted(mapping)}" + ) from None + + +PCMLike = Union["ctypes.Array", bytes, bytearray, memoryview, Sequence[float]] + + +def _pcm_to_carray(pcm: PCMLike): + """Copy *pcm* into a ``c_float`` array, returning ``(array, n_samples)``.""" + if isinstance(pcm, ctypes.Array) and pcm._type_ is ctypes.c_float: + n = len(pcm) + if n == 0: + raise InvalidArgument("empty PCM buffer") + return pcm, n + + try: + mv = memoryview(pcm) + except TypeError: + seq = [float(x) for x in pcm] + if not seq: + raise InvalidArgument("empty PCM buffer") + return (ctypes.c_float * len(seq))(*seq), len(seq) + + with mv: + if not mv.contiguous: + raise InvalidArgument("PCM buffer must be C-contiguous") + if mv.format == "f": + floats = mv + elif mv.itemsize == 1: # raw bytes: interpret as little-endian float32 + raw = mv.cast("B") + if raw.nbytes % 4: + raise InvalidArgument( + "raw PCM byte buffer length is not a multiple of 4 (float32)" + ) + floats = raw.cast("f") + else: + raise InvalidArgument( + f"PCM must be float32 (got buffer format {mv.format!r}); convert " + "with array('f', ...) or numpy.asarray(x, dtype='float32')" + ) + n = floats.shape[0] + if n == 0: + raise InvalidArgument("empty PCM buffer") + return (ctypes.c_float * n).from_buffer_copy(floats), n + + +# --- result value objects ------------------------------------------------- + + +@dataclass(frozen=True) +class Segment: + text: str + t0_ms: int + t1_ms: int + first_word: int + n_words: int + first_token: int + n_tokens: int + + +@dataclass(frozen=True) +class Word: + text: str + t0_ms: int + t1_ms: int + seg_index: int + first_token: int + n_tokens: int + + +@dataclass(frozen=True) +class Token: + text: str + id: int + p: float + t0_ms: int + t1_ms: int + seg_index: int + word_index: int + + +@dataclass(frozen=True) +class Timings: + load_ms: float + mel_ms: float + encode_ms: float + decode_ms: float + + +@dataclass(frozen=True) +class Capabilities: + native_sample_rate: int + languages: tuple + max_timestamp_kind: str + supports_language_detect: bool + supports_translate: bool + supports_streaming: bool + supports_spec_decode: bool + max_audio_ms: int + + +@dataclass(frozen=True) +class Result: + """A fully materialized transcription. Holds no native pointers: every + string and row was copied out of the session before this object was + returned, so it stays valid after later runs.""" + + text: str + language: str + timestamp_kind: str + segments: tuple + words: tuple + tokens: tuple + timings: Timings + + +@dataclass(frozen=True) +class StreamUpdate: + """Per-call change metadata returned by Stream.feed()/finalize().""" + + result_changed: bool + is_final: bool + revision: int + input_received_ms: int + audio_committed_ms: int + buffered_ms: int + committed_changed: bool + tentative_changed: bool + + +@dataclass(frozen=True) +class StreamText: + """The UI-facing text views of an active stream (owned copies). + + ``committed`` is append-only and stable; ``tentative`` is the volatile + suffix; ``display`` is what a UI should show. ``full`` is the raw model + hypothesis (may not be ``committed + tentative`` after a revision).""" + + full: str + committed: str + tentative: str + + @property + def display(self) -> str: + return self.committed + self.tentative + + +# --- materialization helpers ---------------------------------------------- +# Build owned value objects by copying out of the ctypes row structs. Shared by +# the single-result and per-utterance (batch) accessor paths. + + +def _segment_from(s) -> Segment: + return Segment( + text=_decode(s.text), t0_ms=s.t0_ms, t1_ms=s.t1_ms, + first_word=s.first_word, n_words=s.n_words, + first_token=s.first_token, n_tokens=s.n_tokens, + ) + + +def _word_from(w) -> Word: + return Word( + text=_decode(w.text), t0_ms=w.t0_ms, t1_ms=w.t1_ms, + seg_index=w.seg_index, first_token=w.first_token, n_tokens=w.n_tokens, + ) + + +def _token_from(t) -> Token: + return Token( + text=_decode(t.text), id=t.id, p=t.p, t0_ms=t.t0_ms, t1_ms=t.t1_ms, + seg_index=t.seg_index, word_index=t.word_index, + ) + + +def _timings_from(tm) -> Timings: + return Timings( + load_ms=tm.load_ms, mel_ms=tm.mel_ms, + encode_ms=tm.encode_ms, decode_ms=tm.decode_ms, + ) + + +def _stream_update_from(u) -> StreamUpdate: + return StreamUpdate( + result_changed=bool(u.result_changed), is_final=bool(u.is_final), + revision=u.revision, input_received_ms=u.input_received_ms, + audio_committed_ms=u.audio_committed_ms, buffered_ms=u.buffered_ms, + committed_changed=bool(u.committed_changed), + tentative_changed=bool(u.tentative_changed), + ) + + +def _build_run_params(task, language, target_language, timestamps, keep_special_tags): + params = _RunParams() + _lib.transcribe_run_params_init(_byref(params)) + params.task = _enum(_TASKS, task, "task") + params.timestamps = _enum(_TIMESTAMPS, timestamps, "timestamps") + params.language = language.encode("utf-8") if language else None + params.target_language = target_language.encode("utf-8") if target_language else None + params.keep_special_tags = keep_special_tags + return params + + +# --- family extensions ---------------------------------------------------- + + +class FamilyExtension: + """Base for typed family-extension options. + + A family extension carries family-specific knobs alongside a run or stream + on a model that accepts them. Subclasses set the slot, kind, ctypes struct, + and init function, and implement ``_apply()`` to copy their overrides onto + the initialized struct (only fields the caller set override the family + defaults). Pass an instance as ``family=`` to Session.run()/stream(); the + model is probed first and a clear error is raised if it does not accept it. + + Adding another family is one subclass: point it at the generated + ``transcribe__*_ext`` struct, its init function, and its + ``TRANSCRIBE_EXT_KIND_*`` constant.""" + + _slot: str = "" + _kind: int = 0 + _struct = None + _init: str = "" + + def _build(self): + ext = self._struct() + getattr(_lib, self._init)(_byref(ext)) + self._apply(ext) + return ext + + def _apply(self, ext) -> None: + raise NotImplementedError + + +class WhisperRunOptions(FamilyExtension): + """Whisper run-extension options (run slot): initial prompt, temperature + fallback, and decode thresholds. Only fields you set override the family + defaults; the rest keep the values transcribe_whisper_run_ext_init stamps.""" + + _slot = "run" + _kind = _generated.TRANSCRIBE_EXT_KIND_WHISPER_RUN + _struct = _generated.transcribe_whisper_run_ext + _init = "transcribe_whisper_run_ext_init" + + def __init__(self, *, initial_prompt: "Union[str, None]" = None, + condition_on_prev_tokens: "Union[bool, None]" = None, + temperature: "Union[float, None]" = None, + temperature_inc: "Union[float, None]" = None, + compression_ratio_thold: "Union[float, None]" = None, + logprob_thold: "Union[float, None]" = None, + no_speech_thold: "Union[float, None]" = None, + max_prev_context_tokens: "Union[int, None]" = None, + seed: "Union[int, None]" = None, + max_initial_timestamp: "Union[float, None]" = None): + self.initial_prompt = initial_prompt + self.condition_on_prev_tokens = condition_on_prev_tokens + self.temperature = temperature + self.temperature_inc = temperature_inc + self.compression_ratio_thold = compression_ratio_thold + self.logprob_thold = logprob_thold + self.no_speech_thold = no_speech_thold + self.max_prev_context_tokens = max_prev_context_tokens + self.seed = seed + self.max_initial_timestamp = max_initial_timestamp + + def _apply(self, ext) -> None: + if self.initial_prompt is not None: + ext.initial_prompt = self.initial_prompt.encode("utf-8") + if self.condition_on_prev_tokens is not None: + ext.condition_on_prev_tokens = self.condition_on_prev_tokens + if self.temperature is not None: + ext.temperature = self.temperature + if self.temperature_inc is not None: + ext.temperature_inc = self.temperature_inc + if self.compression_ratio_thold is not None: + ext.compression_ratio_thold = self.compression_ratio_thold + if self.logprob_thold is not None: + ext.logprob_thold = self.logprob_thold + if self.no_speech_thold is not None: + ext.no_speech_thold = self.no_speech_thold + if self.max_prev_context_tokens is not None: + ext.max_prev_context_tokens = self.max_prev_context_tokens + if self.seed is not None: + ext.seed = self.seed + if self.max_initial_timestamp is not None: + ext.max_initial_timestamp = self.max_initial_timestamp + + +class MoonshineStreamingOptions(FamilyExtension): + """Moonshine-streaming stream-extension options (stream slot).""" + + _slot = "stream" + _kind = _generated.TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM + _struct = _generated.transcribe_moonshine_streaming_stream_ext + _init = "transcribe_moonshine_streaming_stream_ext_init" + + def __init__(self, *, min_decode_interval_ms: "Union[int, None]" = None): + self.min_decode_interval_ms = min_decode_interval_ms + + def _apply(self, ext) -> None: + if self.min_decode_interval_ms is not None: + ext.min_decode_interval_ms = self.min_decode_interval_ms + + +class ParakeetStreamOptions(FamilyExtension): + """Parakeet cache-aware streaming options (stream slot).""" + + _slot = "stream" + _kind = _generated.TRANSCRIBE_EXT_KIND_PARAKEET_STREAM + _struct = _generated.transcribe_parakeet_stream_ext + _init = "transcribe_parakeet_stream_ext_init" + + def __init__(self, *, att_context_right: "Union[int, None]" = None): + self.att_context_right = att_context_right + + def _apply(self, ext) -> None: + if self.att_context_right is not None: + ext.att_context_right = self.att_context_right + + +class ParakeetBufferedStreamOptions(FamilyExtension): + """Parakeet chunked-attention buffered streaming options (stream slot). + A field left at None keeps the family default (the C sentinel -1).""" + + _slot = "stream" + _kind = _generated.TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM + _struct = _generated.transcribe_parakeet_buffered_stream_ext + _init = "transcribe_parakeet_buffered_stream_ext_init" + + def __init__(self, *, left_ms: "Union[int, None]" = None, + chunk_ms: "Union[int, None]" = None, + right_ms: "Union[int, None]" = None): + self.left_ms = left_ms + self.chunk_ms = chunk_ms + self.right_ms = right_ms + + def _apply(self, ext) -> None: + if self.left_ms is not None: + ext.left_ms = self.left_ms + if self.chunk_ms is not None: + ext.chunk_ms = self.chunk_ms + if self.right_ms is not None: + ext.right_ms = self.right_ms + + +class VoxtralRealtimeStreamOptions(FamilyExtension): + """Voxtral-realtime streaming options (stream slot).""" + + _slot = "stream" + _kind = _generated.TRANSCRIBE_EXT_KIND_VOXTRAL_REALTIME_STREAM + _struct = _generated.transcribe_voxtral_realtime_stream_ext + _init = "transcribe_voxtral_realtime_stream_ext_init" + + def __init__(self, *, num_delay_tokens: "Union[int, None]" = None, + min_decode_interval_ms: "Union[int, None]" = None): + self.num_delay_tokens = num_delay_tokens + self.min_decode_interval_ms = min_decode_interval_ms + + def _apply(self, ext) -> None: + if self.num_delay_tokens is not None: + ext.num_delay_tokens = self.num_delay_tokens + if self.min_decode_interval_ms is not None: + ext.min_decode_interval_ms = self.min_decode_interval_ms + + +# --- high-level handles --------------------------------------------------- + + +class Model: + """A loaded model. Thread-safe to share across sessions; must outlive them.""" + + def __init__(self, path: "Union[str, os.PathLike]", *, + backend: Backend = "auto", gpu_device: int = 0): + params = _ModelLoadParams() + _lib.transcribe_model_load_params_init(_byref(params)) + params.backend = _enum(_BACKENDS, backend, "backend") + params.gpu_device = gpu_device + + handle = ctypes.c_void_p() + status = _lib.transcribe_model_load_file( + os.fspath(path).encode("utf-8"), _byref(params), _byref(handle) + ) + _check(status, f"loading model {os.fspath(path)!r}") + if not handle.value: + raise ModelLoadError(f"model load returned a null handle for {path!r}") + self._handle = handle + + @property + def _h(self) -> ctypes.c_void_p: + if self._handle is None: + raise TranscribeError("model is closed") + return self._handle + + @property + def arch(self) -> str: + return _decode(_lib.transcribe_model_arch_string(self._h)) + + @property + def variant(self) -> str: + return _decode(_lib.transcribe_model_variant_string(self._h)) + + @property + def backend(self) -> str: + return _decode(_lib.transcribe_model_backend(self._h)) + + @property + def capabilities(self) -> Capabilities: + caps = _Capabilities() + _lib.transcribe_capabilities_init(_byref(caps)) + _check( + _lib.transcribe_model_get_capabilities(self._h, _byref(caps)), + "reading capabilities", + ) + languages = [] + if caps.languages and caps.n_languages > 0: + for i in range(caps.n_languages): + languages.append(_decode(caps.languages[i])) + return Capabilities( + native_sample_rate=caps.native_sample_rate, + languages=tuple(languages), + max_timestamp_kind=_TIMESTAMP_NAMES.get(caps.max_timestamp_kind, "unknown"), + supports_language_detect=bool(caps.supports_language_detect), + supports_translate=bool(caps.supports_translate), + supports_streaming=bool(caps.supports_streaming), + supports_spec_decode=bool(caps.supports_spec_decode), + max_audio_ms=caps.max_audio_ms, + ) + + def supports(self, feature: Feature) -> bool: + """Whether the model exposes a behavioral feature (initial prompt, + temperature fallback, long-form, cancellation, pnc, itn).""" + return bool(_lib.transcribe_model_supports( + self._h, _enum(_FEATURES, feature, "feature"))) + + def accepts(self, options: "FamilyExtension") -> bool: + """Whether the model accepts a family extension on its slot.""" + return bool(_lib.transcribe_model_accepts_ext_kind( + self._h, _EXT_SLOTS[options._slot], options._kind)) + + def session(self, *, n_threads: int = 0, kv_type: KVType = "auto", + n_ctx: int = 0) -> "Session": + return Session(self, n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx) + + def close(self) -> None: + if getattr(self, "_handle", None) is not None: + _lib.transcribe_model_free(self._handle) + self._handle = None + + def __enter__(self) -> "Model": + return self + + def __exit__(self, *exc) -> None: + self.close() + + def __del__(self): + try: + self.close() + except Exception: + pass + + +class Session: + """A single transcription context bound to one Model. Not thread-safe; use + one per thread (sharing the Model is fine).""" + + def __init__(self, model: Model, *, n_threads: int = 0, kv_type: KVType = "auto", + n_ctx: int = 0): + self._model = model # keep the model alive for the session's lifetime + params = _SessionParams() + _lib.transcribe_session_params_init(_byref(params)) + params.n_threads = n_threads + params.kv_type = _enum(_KV_TYPES, kv_type, "kv_type") + params.n_ctx = n_ctx + + handle = ctypes.c_void_p() + status = _lib.transcribe_session_init(model._h, _byref(params), _byref(handle)) + _check(status, "opening session") + if not handle.value: + raise TranscribeError("session init returned a null handle") + self._handle = handle + + # Cancellation: an abort callback polled at chunk/decode boundaries + # returns True once cancel() is requested, so another thread can abort an + # in-flight run/stream. Bind the Event (not self) into the trampoline to + # avoid a reference cycle that would defer __del__. + self._cancel = threading.Event() + _event = self._cancel + self._abort_trampoline = _ABORT_CFUNC(lambda _ud: _event.is_set()) + _lib.transcribe_set_abort_callback(self._handle, self._abort_trampoline, None) + + @property + def _h(self) -> ctypes.c_void_p: + if self._handle is None: + raise TranscribeError("session is closed") + return self._handle + + def _resolve_family(self, family: "FamilyExtension", slot: str): + """Validate + probe a family extension and return its built ctypes + struct (which the caller keeps alive and points params.family at).""" + if not isinstance(family, FamilyExtension): + raise InvalidArgument("family must be a FamilyExtension instance") + if family._slot != slot: + raise InvalidArgument( + f"{type(family).__name__} is a {family._slot!r}-slot extension and " + f"cannot be used on the {slot!r} slot") + if not _lib.transcribe_model_accepts_ext_kind( + self._model._h, _EXT_SLOTS[slot], family._kind): + raise UnsupportedRequest( + f"this model does not accept {type(family).__name__} on the " + f"{slot!r} slot") + return family._build() + + def run(self, pcm: PCMLike, *, task: Task = "transcribe", + language: "Union[str, None]" = None, + target_language: "Union[str, None]" = None, + timestamps: Timestamps = "none", + keep_special_tags: bool = False, + family: "Union[FamilyExtension, None]" = None) -> Result: + """Transcribe 16 kHz mono float32 PCM and return a materialized Result. + + ``family`` is an optional family-specific extension (e.g. + WhisperRunOptions) carrying per-run knobs for models that accept it.""" + self._cancel.clear() + array, n_samples = _pcm_to_carray(pcm) + params = _build_run_params(task, language, target_language, timestamps, + keep_special_tags) + ext = self._resolve_family(family, "run") if family is not None else None + if ext is not None: + params.family = ctypes.cast( + _byref(ext), ctypes.POINTER(_generated.transcribe_ext)) + _check(_lib.transcribe_run(self._h, array, n_samples, _byref(params)), + "transcribe_run") + return self._materialize() + + def run_batch(self, pcms: "Sequence[PCMLike]", *, task: Task = "transcribe", + language: "Union[str, None]" = None, + target_language: "Union[str, None]" = None, + timestamps: Timestamps = "none", + keep_special_tags: bool = False) -> "list": + """Transcribe several utterances in one dispatch — one Result each. + + Families with a batched compute path process every utterance in a single + device dispatch (≈2x throughput on an underused GPU); others fall back to + running them in turn, so every model accepts this. Raises if any single + utterance failed (its index is in the message).""" + self._cancel.clear() + pcms = list(pcms) + if not pcms: + raise InvalidArgument("run_batch requires at least one PCM buffer") + + arrays = [] # keep the per-utterance float arrays alive across the call + ptrs = (ctypes.POINTER(ctypes.c_float) * len(pcms))() + counts = (ctypes.c_int * len(pcms))() + for k, pcm in enumerate(pcms): + arr, n = _pcm_to_carray(pcm) + arrays.append(arr) + ptrs[k] = ctypes.cast(arr, ctypes.POINTER(ctypes.c_float)) + counts[k] = n + + params = _build_run_params(task, language, target_language, timestamps, + keep_special_tags) + _check( + _lib.transcribe_run_batch(self._h, ptrs, counts, len(pcms), _byref(params)), + "transcribe_run_batch", + ) + + results = [] + for i in range(_lib.transcribe_batch_n_results(self._h)): + _check(_lib.transcribe_batch_status(self._h, i), f"utterance {i} in batch") + results.append(self._materialize(i)) + return results + + def stream(self, *, task: Task = "transcribe", language: "Union[str, None]" = None, + target_language: "Union[str, None]" = None, timestamps: Timestamps = "none", + keep_special_tags: bool = False, commit_policy: CommitPolicy = "auto", + stable_prefix_agreement_n: int = 0, + family: "Union[FamilyExtension, None]" = None) -> "Stream": + """Begin streaming on this session and return a Stream to feed audio to. + + Requires a model whose capabilities advertise ``supports_streaming``; + otherwise raises NotImplementedByModel. ``family`` is an optional + family-specific stream extension (e.g. MoonshineStreamingOptions). The + session is single-threaded and runs at most one stream at a time. Use + the Stream as a context manager so it is reset when you are done.""" + self._cancel.clear() + run_params = _build_run_params(task, language, target_language, timestamps, + keep_special_tags) + sp = _StreamParams() + _lib.transcribe_stream_params_init(_byref(sp)) + sp.commit_policy = _enum(_COMMIT_POLICIES, commit_policy, "commit_policy") + sp.stable_prefix_agreement_n = stable_prefix_agreement_n + ext = self._resolve_family(family, "stream") if family is not None else None + if ext is not None: + sp.family = ctypes.cast( + _byref(ext), ctypes.POINTER(_generated.transcribe_ext)) + _check( + _lib.transcribe_stream_begin(self._h, _byref(run_params), _byref(sp)), + "transcribe_stream_begin", + ) + return Stream(self) + + def _materialize(self, utt: "Union[int, None]" = None) -> Result: + """Copy out one result. utt is None for the single-result accessors, or + an utterance index for the batch accessors (index 0 aliases the single + result after a plain run, so both paths share this code).""" + h = self._h + if utt is None: + n_seg = lambda: _lib.transcribe_n_segments(h) + get_seg = lambda j, out: _lib.transcribe_get_segment(h, j, out) + n_word = lambda: _lib.transcribe_n_words(h) + get_word = lambda j, out: _lib.transcribe_get_word(h, j, out) + n_tok = lambda: _lib.transcribe_n_tokens(h) + get_tok = lambda j, out: _lib.transcribe_get_token(h, j, out) + full_text = _lib.transcribe_full_text(h) + language = _lib.transcribe_detected_language(h) + kind = _lib.transcribe_returned_timestamp_kind(h) + get_tim = lambda out: _lib.transcribe_get_timings(h, out) + else: + n_seg = lambda: _lib.transcribe_batch_n_segments(h, utt) + get_seg = lambda j, out: _lib.transcribe_batch_get_segment(h, utt, j, out) + n_word = lambda: _lib.transcribe_batch_n_words(h, utt) + get_word = lambda j, out: _lib.transcribe_batch_get_word(h, utt, j, out) + n_tok = lambda: _lib.transcribe_batch_n_tokens(h, utt) + get_tok = lambda j, out: _lib.transcribe_batch_get_token(h, utt, j, out) + full_text = _lib.transcribe_batch_full_text(h, utt) + language = _lib.transcribe_batch_detected_language(h, utt) + kind = _lib.transcribe_batch_returned_timestamp_kind(h, utt) + get_tim = lambda out: _lib.transcribe_batch_get_timings(h, utt, out) + + segments = [] + for j in range(n_seg()): + s = _Segment() + _lib.transcribe_segment_init(_byref(s)) + get_seg(j, _byref(s)) + segments.append(_segment_from(s)) + + words = [] + for j in range(n_word()): + w = _Word() + _lib.transcribe_word_init(_byref(w)) + get_word(j, _byref(w)) + words.append(_word_from(w)) + + tokens = [] + for j in range(n_tok()): + tok = _Token() + _lib.transcribe_token_init(_byref(tok)) + get_tok(j, _byref(tok)) + tokens.append(_token_from(tok)) + + tm = _Timings() + _lib.transcribe_timings_init(_byref(tm)) + get_tim(_byref(tm)) + + return Result( + text=_decode(full_text), + language=_decode(language), + timestamp_kind=_TIMESTAMP_NAMES.get(kind, "unknown"), + segments=tuple(segments), + words=tuple(words), + tokens=tuple(tokens), + timings=_timings_from(tm), + ) + + def cancel(self) -> None: + """Request cancellation of an in-flight run/stream from another thread. + The active call aborts at the next chunk/decode boundary and raises; + ``was_aborted`` then reports True. The flag is cleared at the start of + the next run/stream.""" + self._cancel.set() + + @property + def was_aborted(self) -> bool: + """True if the most recent call was ended by cancel().""" + return bool(_lib.transcribe_was_aborted(self._h)) + + def close(self) -> None: + if getattr(self, "_handle", None) is not None: + _lib.transcribe_session_free(self._handle) + self._handle = None + + def __enter__(self) -> "Session": + return self + + def __exit__(self, *exc) -> None: + self.close() + + def __del__(self): + try: + self.close() + except Exception: + pass + + +class Stream: + """An active streaming transcription on a Session. + + Feed audio in chunks with ``feed()``, read the committed/tentative text with + ``text()``, and call ``finalize()`` when the audio ends. The session is + returned to idle on context-manager exit (or ``reset()``).""" + + def __init__(self, session: Session): + self._session = session # keep the session (and its model) alive + self._active = True + + @property + def _h(self) -> ctypes.c_void_p: + if not self._active: + raise TranscribeError("stream has been reset") + return self._session._h + + def feed(self, pcm: PCMLike) -> StreamUpdate: + """Feed a chunk of 16 kHz mono float32 PCM; returns change metadata.""" + array, n_samples = _pcm_to_carray(pcm) + update = _StreamUpdate() + _lib.transcribe_stream_update_init(_byref(update)) + _check(_lib.transcribe_stream_feed(self._h, array, n_samples, _byref(update)), + "transcribe_stream_feed") + return _stream_update_from(update) + + def finalize(self) -> StreamUpdate: + """Signal end of audio and flush the final hypothesis.""" + update = _StreamUpdate() + _lib.transcribe_stream_update_init(_byref(update)) + _check(_lib.transcribe_stream_finalize(self._h, _byref(update)), + "transcribe_stream_finalize") + return _stream_update_from(update) + + def text(self) -> StreamText: + """Current committed / tentative / full text views (owned copies).""" + txt = _StreamText() + _lib.transcribe_stream_text_init(_byref(txt)) + _check(_lib.transcribe_stream_get_text(self._h, _byref(txt)), + "transcribe_stream_get_text") + return StreamText( + full=_decode(txt.full_text), + committed=_decode(txt.committed_text), + tentative=_decode(txt.tentative_text), + ) + + @property + def state(self) -> str: + """``"idle"`` / ``"active"`` / ``"finished"`` / ``"failed"``.""" + return _STREAM_STATES.get(_lib.transcribe_stream_get_state(self._h), "unknown") + + @property + def revision(self) -> int: + return _lib.transcribe_stream_revision(self._h) + + def reset(self) -> None: + """Return the session to idle, discarding stream state. Idempotent.""" + if self._active: + _lib.transcribe_stream_reset(self._session._h) + self._active = False + + def __enter__(self) -> "Stream": + return self + + def __exit__(self, *exc) -> None: + self.reset() + + +def transcribe( + model: "Union[Model, str, os.PathLike]", + pcm: PCMLike, + *, + backend: Backend = "auto", + gpu_device: int = 0, + n_threads: int = 0, + kv_type: KVType = "auto", + n_ctx: int = 0, + task: Task = "transcribe", + language: "Union[str, None]" = None, + target_language: "Union[str, None]" = None, + timestamps: Timestamps = "none", + keep_special_tags: bool = False, +) -> Result: + """Transcribe *pcm* in one call and return a materialized Result. + + *model* may be a path (loaded and freed within this call) or an existing + Model (reused and left open). Loading a model is not free, so to transcribe + many clips keep a Model and call ``model.session().run(...)`` yourself; this + helper is for the one-shot case. ``backend`` / ``gpu_device`` apply only when + *model* is a path — they are ignored when an already-loaded Model is passed. + """ + session_opts = dict(n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx) + run_opts = dict(task=task, language=language, target_language=target_language, + timestamps=timestamps, keep_special_tags=keep_special_tags) + + if isinstance(model, Model): + with model.session(**session_opts) as session: + return session.run(pcm, **run_opts) -__all__ = ["__version__"] + with Model(model, backend=backend, gpu_device=gpu_device) as owned: + with owned.session(**session_opts) as session: + return session.run(pcm, **run_opts) diff --git a/bindings/python/src/transcribe_cpp/_abi.py b/bindings/python/src/transcribe_cpp/_abi.py new file mode 100644 index 00000000..2bb9bfb4 --- /dev/null +++ b/bindings/python/src/transcribe_cpp/_abi.py @@ -0,0 +1,77 @@ +"""Load-time ABI verification of the generated ctypes layer. + +Two independent checks, both required before the high-level API touches a struct: + +1. **Offset self-check** — every generated Structure's ctypes size, alignment, + and per-field offsets must equal the C-compiler layout libclang captured at + generation time (``_generated.STRUCT_LAYOUT``). This catches a wrong type in + the generated module on the running platform. + +2. **Native agreement** — for every struct with a ``transcribe_abi_struct`` id, + ctypes size/alignment must equal what the *loaded* native library reports + (``transcribe_abi_struct_size``/``_align``). This catches a committed module + that is stale relative to the actual ``.dylib``/``.so`` in this process. + +A ctypes layout mismatch corrupts memory silently; turning it into an +ImportError here is the safety net behind the generated FFI. +""" + +from __future__ import annotations + +import ctypes + +from . import _generated +from .errors import AbiError + + +def verify_layouts(lib: ctypes.CDLL) -> None: + """Raise AbiError if the generated layer disagrees with itself or the lib.""" + mismatches: list[str] = [] + + # 1. ctypes layout vs the captured C-compiler layout. + for name, layout in _generated.STRUCT_LAYOUT.items(): + cls = getattr(_generated, name) + if ctypes.sizeof(cls) != layout["size"]: + mismatches.append( + f"{name}: size {ctypes.sizeof(cls)} (ctypes) != {layout['size']} (generated)" + ) + if ctypes.alignment(cls) != layout["align"]: + mismatches.append( + f"{name}: alignment {ctypes.alignment(cls)} (ctypes) != " + f"{layout['align']} (generated)" + ) + for field, offset in layout["offsets"].items(): + actual = getattr(cls, field).offset + if actual != offset: + mismatches.append( + f"{name}.{field}: offset {actual} (ctypes) != {offset} (generated)" + ) + + # 2. ctypes size/alignment vs the loaded native library. + for name, abi_id in _generated.ABI_STRUCT_IDS.items(): + cls = getattr(_generated, name) + native_size = lib.transcribe_abi_struct_size(abi_id) + native_align = lib.transcribe_abi_struct_align(abi_id) + if native_size == 0: + mismatches.append( + f"{name}: native library does not know abi id {abi_id} " + "(library older than this binding)" + ) + continue + if ctypes.sizeof(cls) != native_size: + mismatches.append( + f"{name}: size {ctypes.sizeof(cls)} (binding) != {native_size} (native)" + ) + if ctypes.alignment(cls) != native_align: + mismatches.append( + f"{name}: alignment {ctypes.alignment(cls)} (binding) != " + f"{native_align} (native)" + ) + + if mismatches: + raise AbiError( + "transcribe_cpp ABI layout check failed — the generated binding and " + "native library disagree on struct layout. This is a version/build " + "skew or a stale generated layer; do not run against this library.\n " + + "\n ".join(mismatches) + ) diff --git a/bindings/python/src/transcribe_cpp/_generated.py b/bindings/python/src/transcribe_cpp/_generated.py new file mode 100644 index 00000000..e1897661 --- /dev/null +++ b/bindings/python/src/transcribe_cpp/_generated.py @@ -0,0 +1,374 @@ +"""Low-level ctypes FFI for transcribe.cpp — AUTOGENERATED. DO NOT EDIT. + +Regenerate with: + uv run --no-project --with 'libclang==18.1.1' \ + bindings/python/_generate/generate.py + +Source: include/transcribe/extensions.h +libclang: 18.1.1 +""" + +import ctypes as _c + +# 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 = "287ebebdeb529e76" + +# === enum constants === +TRANSCRIBE_OK = 0 +TRANSCRIBE_ERR_INVALID_ARG = 1 +TRANSCRIBE_ERR_NOT_IMPLEMENTED = 2 +TRANSCRIBE_ERR_FILE_NOT_FOUND = 3 +TRANSCRIBE_ERR_GGUF = 4 +TRANSCRIBE_ERR_UNSUPPORTED_ARCH = 5 +TRANSCRIBE_ERR_UNSUPPORTED_VARIANT = 6 +TRANSCRIBE_ERR_OOM = 7 +TRANSCRIBE_ERR_BACKEND = 8 +TRANSCRIBE_ERR_SAMPLE_RATE = 9 +TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE = 10 +TRANSCRIBE_ERR_UNSUPPORTED_TASK = 11 +TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS = 12 +TRANSCRIBE_ERR_ABORTED = 13 +TRANSCRIBE_ERR_BAD_STRUCT_SIZE = 14 +TRANSCRIBE_ERR_UNSUPPORTED_PNC = 15 +TRANSCRIBE_ERR_UNSUPPORTED_ITN = 16 +TRANSCRIBE_ERR_INPUT_TOO_LONG = 17 +TRANSCRIBE_ERR_OUTPUT_TRUNCATED = 18 +TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0 +TRANSCRIBE_ABI_SESSION_PARAMS = 1 +TRANSCRIBE_ABI_RUN_PARAMS = 2 +TRANSCRIBE_ABI_STREAM_PARAMS = 3 +TRANSCRIBE_ABI_CAPABILITIES = 4 +TRANSCRIBE_ABI_TIMINGS = 5 +TRANSCRIBE_ABI_SEGMENT = 6 +TRANSCRIBE_ABI_WORD = 7 +TRANSCRIBE_ABI_TOKEN = 8 +TRANSCRIBE_ABI_STREAM_UPDATE = 9 +TRANSCRIBE_ABI_STREAM_TEXT = 10 +TRANSCRIBE_ABI_SESSION_LIMITS = 11 +TRANSCRIBE_ABI_EXT = 12 +TRANSCRIBE_LOG_LEVEL_NONE = 0 +TRANSCRIBE_LOG_LEVEL_INFO = 1 +TRANSCRIBE_LOG_LEVEL_WARN = 2 +TRANSCRIBE_LOG_LEVEL_ERROR = 3 +TRANSCRIBE_LOG_LEVEL_DEBUG = 4 +TRANSCRIBE_LOG_LEVEL_CONT = 5 +TRANSCRIBE_TASK_TRANSCRIBE = 0 +TRANSCRIBE_TASK_TRANSLATE = 1 +TRANSCRIBE_TIMESTAMPS_NONE = 0 +TRANSCRIBE_TIMESTAMPS_AUTO = 1 +TRANSCRIBE_TIMESTAMPS_SEGMENT = 2 +TRANSCRIBE_TIMESTAMPS_WORD = 3 +TRANSCRIBE_TIMESTAMPS_TOKEN = 4 +TRANSCRIBE_KV_TYPE_AUTO = 0 +TRANSCRIBE_KV_TYPE_F32 = 1 +TRANSCRIBE_KV_TYPE_F16 = 2 +TRANSCRIBE_PNC_MODE_DEFAULT = 0 +TRANSCRIBE_PNC_MODE_OFF = 1 +TRANSCRIBE_PNC_MODE_ON = 2 +TRANSCRIBE_ITN_MODE_DEFAULT = 0 +TRANSCRIBE_ITN_MODE_OFF = 1 +TRANSCRIBE_ITN_MODE_ON = 2 +TRANSCRIBE_EXT_SLOT_RUN = 0 +TRANSCRIBE_EXT_SLOT_STREAM = 1 +TRANSCRIBE_BACKEND_AUTO = 0 +TRANSCRIBE_BACKEND_CPU = 1 +TRANSCRIBE_BACKEND_METAL = 2 +TRANSCRIBE_BACKEND_VULKAN = 3 +TRANSCRIBE_BACKEND_CPU_ACCEL = 4 +TRANSCRIBE_BACKEND_CUDA = 5 +TRANSCRIBE_FEATURE_INITIAL_PROMPT = 0 +TRANSCRIBE_FEATURE_TEMPERATURE_FALLBACK = 1 +TRANSCRIBE_FEATURE_LONG_FORM = 2 +TRANSCRIBE_FEATURE_CANCELLATION = 3 +TRANSCRIBE_FEATURE_PNC = 4 +TRANSCRIBE_FEATURE_ITN = 5 +TRANSCRIBE_STREAM_IDLE = 0 +TRANSCRIBE_STREAM_ACTIVE = 1 +TRANSCRIBE_STREAM_FINISHED = 2 +TRANSCRIBE_STREAM_FAILED = 3 +TRANSCRIBE_STREAM_COMMIT_AUTO = 0 +TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE = 1 +TRANSCRIBE_STREAM_COMMIT_STABLE_PREFIX = 2 +TRANSCRIBE_WHISPER_PROMPT_FIRST_SEGMENT = 0 +TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS = 1 + +# === macro constants (integer object-like macros) === +TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM = 1414746957 +TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM = 1396853584 +TRANSCRIBE_EXT_KIND_PARAKEET_STREAM = 1414744912 +TRANSCRIBE_EXT_KIND_VOXTRAL_REALTIME_STREAM = 1414746710 +TRANSCRIBE_EXT_KIND_WHISPER_RUN = 1314015319 +TRANSCRIBE_VERSION_MAJOR = 0 +TRANSCRIBE_VERSION_MINOR = 0 +TRANSCRIBE_VERSION_PATCH = 1 + +# === structs === +class transcribe_ext(_c.Structure): + pass +class transcribe_model_load_params(_c.Structure): + pass +class transcribe_session_params(_c.Structure): + pass +class transcribe_run_params(_c.Structure): + pass +class transcribe_capabilities(_c.Structure): + pass +class transcribe_session_limits(_c.Structure): + pass +class transcribe_stream_params(_c.Structure): + pass +class transcribe_stream_update(_c.Structure): + pass +class transcribe_stream_text(_c.Structure): + pass +class transcribe_timings(_c.Structure): + pass +class transcribe_segment(_c.Structure): + pass +class transcribe_word(_c.Structure): + pass +class transcribe_token(_c.Structure): + pass +class transcribe_moonshine_streaming_stream_ext(_c.Structure): + pass +class transcribe_parakeet_stream_ext(_c.Structure): + pass +class transcribe_parakeet_buffered_stream_ext(_c.Structure): + pass +class transcribe_voxtral_realtime_stream_ext(_c.Structure): + pass +class transcribe_whisper_run_ext(_c.Structure): + pass +class transcribe_whisper_chunk_trace(_c.Structure): + pass + +transcribe_ext._fields_ = [("size", _c.c_uint64), ("kind", _c.c_uint32)] +transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("gpu_device", _c.c_int)] +transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)] +transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)] +transcribe_capabilities._fields_ = [("struct_size", _c.c_uint64), ("native_sample_rate", _c.c_int32), ("n_languages", _c.c_int), ("languages", _c.POINTER(_c.c_char_p)), ("max_timestamp_kind", _c.c_int), ("supports_language_detect", _c.c_bool), ("supports_translate", _c.c_bool), ("supports_streaming", _c.c_bool), ("supports_spec_decode", _c.c_bool), ("max_audio_ms", _c.c_int64)] +transcribe_session_limits._fields_ = [("struct_size", _c.c_uint64), ("effective_n_ctx", _c.c_int32), ("effective_max_audio_ms", _c.c_int64), ("max_kv_bytes", _c.c_int64)] +transcribe_stream_params._fields_ = [("struct_size", _c.c_uint64), ("family", _c.POINTER(transcribe_ext)), ("commit_policy", _c.c_int), ("stable_prefix_agreement_n", _c.c_uint32)] +transcribe_stream_update._fields_ = [("struct_size", _c.c_uint64), ("result_changed", _c.c_bool), ("is_final", _c.c_bool), ("revision", _c.c_int32), ("input_received_ms", _c.c_int64), ("audio_committed_ms", _c.c_int64), ("buffered_ms", _c.c_int64), ("committed_changed", _c.c_bool), ("tentative_changed", _c.c_bool)] +transcribe_stream_text._fields_ = [("struct_size", _c.c_uint64), ("full_text", _c.c_char_p), ("full_text_bytes", _c.c_uint64), ("committed_text", _c.c_char_p), ("committed_text_bytes", _c.c_uint64), ("tentative_text", _c.c_char_p), ("tentative_text_bytes", _c.c_uint64), ("raw_tentative_start_bytes", _c.c_uint64)] +transcribe_timings._fields_ = [("struct_size", _c.c_uint64), ("load_ms", _c.c_float), ("mel_ms", _c.c_float), ("encode_ms", _c.c_float), ("decode_ms", _c.c_float)] +transcribe_segment._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("first_word", _c.c_int), ("n_words", _c.c_int), ("first_token", _c.c_int), ("n_tokens", _c.c_int), ("text", _c.c_char_p)] +transcribe_word._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("seg_index", _c.c_int), ("first_token", _c.c_int), ("n_tokens", _c.c_int), ("text", _c.c_char_p)] +transcribe_token._fields_ = [("struct_size", _c.c_uint64), ("id", _c.c_int), ("p", _c.c_float), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("seg_index", _c.c_int), ("word_index", _c.c_int), ("text", _c.c_char_p)] +transcribe_moonshine_streaming_stream_ext._fields_ = [("ext", transcribe_ext), ("min_decode_interval_ms", _c.c_int32)] +transcribe_parakeet_stream_ext._fields_ = [("ext", transcribe_ext), ("att_context_right", _c.c_int32)] +transcribe_parakeet_buffered_stream_ext._fields_ = [("ext", transcribe_ext), ("left_ms", _c.c_int32), ("chunk_ms", _c.c_int32), ("right_ms", _c.c_int32)] +transcribe_voxtral_realtime_stream_ext._fields_ = [("ext", transcribe_ext), ("num_delay_tokens", _c.c_int32), ("min_decode_interval_ms", _c.c_int32)] +transcribe_whisper_run_ext._fields_ = [("ext", transcribe_ext), ("initial_prompt", _c.c_char_p), ("prompt_tokens", _c.POINTER(_c.c_int32)), ("n_prompt_tokens", _c.c_size_t), ("prompt_condition", _c.c_int), ("condition_on_prev_tokens", _c.c_bool), ("max_prev_context_tokens", _c.c_int32), ("temperature", _c.c_float), ("temperature_inc", _c.c_float), ("compression_ratio_thold", _c.c_float), ("logprob_thold", _c.c_float), ("no_speech_thold", _c.c_float), ("seed", _c.c_uint32), ("max_initial_timestamp", _c.c_float)] +transcribe_whisper_chunk_trace._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("temperature_used", _c.c_float), ("compression_ratio", _c.c_float), ("avg_logprob", _c.c_float), ("no_speech_prob", _c.c_float), ("no_speech_triggered", _c.c_bool), ("n_fallbacks", _c.c_int32)] + +# === ABI metadata === +# transcribe_abi_struct id per struct (for the native size/align check). +ABI_STRUCT_IDS = { + 'transcribe_ext': 12, + 'transcribe_model_load_params': 0, + 'transcribe_session_params': 1, + 'transcribe_run_params': 2, + 'transcribe_capabilities': 4, + 'transcribe_session_limits': 11, + 'transcribe_stream_params': 3, + 'transcribe_stream_update': 9, + 'transcribe_stream_text': 10, + 'transcribe_timings': 5, + 'transcribe_segment': 6, + 'transcribe_word': 7, + 'transcribe_token': 8, +} + +# C-compiler layout captured at generation (for offset self-check). +STRUCT_LAYOUT = { + 'transcribe_ext': {'size': 16, 'align': 8, 'offsets': {'size': 0, 'kind': 8}}, + 'transcribe_model_load_params': {'size': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}}, + 'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}}, + 'transcribe_run_params': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'language': 24, 'target_language': 32, 'keep_special_tags': 40, 'family': 48, 'spec_k_drafts': 56}}, + 'transcribe_capabilities': {'size': 40, 'align': 8, 'offsets': {'struct_size': 0, 'native_sample_rate': 8, 'n_languages': 12, 'languages': 16, 'max_timestamp_kind': 24, 'supports_language_detect': 28, 'supports_translate': 29, 'supports_streaming': 30, 'supports_spec_decode': 31, 'max_audio_ms': 32}}, + 'transcribe_session_limits': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'effective_n_ctx': 8, 'effective_max_audio_ms': 16, 'max_kv_bytes': 24}}, + 'transcribe_stream_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'family': 8, 'commit_policy': 16, 'stable_prefix_agreement_n': 20}}, + 'transcribe_stream_update': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 'result_changed': 8, 'is_final': 9, 'revision': 12, 'input_received_ms': 16, 'audio_committed_ms': 24, 'buffered_ms': 32, 'committed_changed': 40, 'tentative_changed': 41}}, + 'transcribe_stream_text': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'full_text': 8, 'full_text_bytes': 16, 'committed_text': 24, 'committed_text_bytes': 32, 'tentative_text': 40, 'tentative_text_bytes': 48, 'raw_tentative_start_bytes': 56}}, + 'transcribe_timings': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'load_ms': 8, 'mel_ms': 12, 'encode_ms': 16, 'decode_ms': 20}}, + 'transcribe_segment': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'first_word': 24, 'n_words': 28, 'first_token': 32, 'n_tokens': 36, 'text': 40}}, + 'transcribe_word': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'seg_index': 24, 'first_token': 28, 'n_tokens': 32, 'text': 40}}, + 'transcribe_token': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 'id': 8, 'p': 12, 't0_ms': 16, 't1_ms': 24, 'seg_index': 32, 'word_index': 36, 'text': 40}}, + 'transcribe_moonshine_streaming_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'min_decode_interval_ms': 16}}, + 'transcribe_parakeet_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'att_context_right': 16}}, + 'transcribe_parakeet_buffered_stream_ext': {'size': 32, 'align': 8, 'offsets': {'ext': 0, 'left_ms': 16, 'chunk_ms': 20, 'right_ms': 24}}, + 'transcribe_voxtral_realtime_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'num_delay_tokens': 16, 'min_decode_interval_ms': 20}}, + 'transcribe_whisper_run_ext': {'size': 80, 'align': 8, 'offsets': {'ext': 0, 'initial_prompt': 16, 'prompt_tokens': 24, 'n_prompt_tokens': 32, 'prompt_condition': 40, 'condition_on_prev_tokens': 44, 'max_prev_context_tokens': 48, 'temperature': 52, 'temperature_inc': 56, 'compression_ratio_thold': 60, 'logprob_thold': 64, 'no_speech_thold': 68, 'seed': 72, 'max_initial_timestamp': 76}}, + 'transcribe_whisper_chunk_trace': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'temperature_used': 24, 'compression_ratio': 28, 'avg_logprob': 32, 'no_speech_prob': 36, 'no_speech_triggered': 40, 'n_fallbacks': 44}}, +} + +# === function prototypes === +def configure(lib): + """Stamp restype/argtypes onto a loaded CDLL.""" + lib.transcribe_abi_struct_align.restype = _c.c_size_t + lib.transcribe_abi_struct_align.argtypes = [_c.c_int] + lib.transcribe_abi_struct_size.restype = _c.c_size_t + lib.transcribe_abi_struct_size.argtypes = [_c.c_int] + lib.transcribe_batch_detected_language.restype = _c.c_char_p + lib.transcribe_batch_detected_language.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_full_text.restype = _c.c_char_p + lib.transcribe_batch_full_text.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_get_segment.restype = _c.c_int + lib.transcribe_batch_get_segment.argtypes = [_c.c_void_p, _c.c_int, _c.c_int, _c.POINTER(transcribe_segment)] + lib.transcribe_batch_get_timings.restype = _c.c_int + lib.transcribe_batch_get_timings.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_timings)] + lib.transcribe_batch_get_token.restype = _c.c_int + lib.transcribe_batch_get_token.argtypes = [_c.c_void_p, _c.c_int, _c.c_int, _c.POINTER(transcribe_token)] + lib.transcribe_batch_get_word.restype = _c.c_int + lib.transcribe_batch_get_word.argtypes = [_c.c_void_p, _c.c_int, _c.c_int, _c.POINTER(transcribe_word)] + lib.transcribe_batch_n_results.restype = _c.c_int + lib.transcribe_batch_n_results.argtypes = [_c.c_void_p] + lib.transcribe_batch_n_segments.restype = _c.c_int + lib.transcribe_batch_n_segments.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_n_tokens.restype = _c.c_int + lib.transcribe_batch_n_tokens.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_n_words.restype = _c.c_int + lib.transcribe_batch_n_words.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_returned_timestamp_kind.restype = _c.c_int + lib.transcribe_batch_returned_timestamp_kind.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_batch_status.restype = _c.c_int + lib.transcribe_batch_status.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_capabilities_init.restype = None + lib.transcribe_capabilities_init.argtypes = [_c.POINTER(transcribe_capabilities)] + lib.transcribe_close.restype = None + lib.transcribe_close.argtypes = [_c.c_void_p] + lib.transcribe_detected_language.restype = _c.c_char_p + lib.transcribe_detected_language.argtypes = [_c.c_void_p] + lib.transcribe_ext_check.restype = _c.c_int + lib.transcribe_ext_check.argtypes = [_c.POINTER(transcribe_ext), _c.c_uint32, _c.c_uint64] + lib.transcribe_full_text.restype = _c.c_char_p + lib.transcribe_full_text.argtypes = [_c.c_void_p] + lib.transcribe_get_model.restype = _c.c_void_p + lib.transcribe_get_model.argtypes = [_c.c_void_p] + lib.transcribe_get_segment.restype = _c.c_int + lib.transcribe_get_segment.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_segment)] + lib.transcribe_get_timings.restype = _c.c_int + lib.transcribe_get_timings.argtypes = [_c.c_void_p, _c.POINTER(transcribe_timings)] + lib.transcribe_get_token.restype = _c.c_int + lib.transcribe_get_token.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_token)] + lib.transcribe_get_whisper_chunk_count.restype = _c.c_int + lib.transcribe_get_whisper_chunk_count.argtypes = [_c.c_void_p] + lib.transcribe_get_whisper_chunk_trace.restype = _c.c_int + lib.transcribe_get_whisper_chunk_trace.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_whisper_chunk_trace)] + lib.transcribe_get_word.restype = _c.c_int + lib.transcribe_get_word.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_word)] + lib.transcribe_log_set.restype = None + lib.transcribe_log_set.argtypes = [_c.CFUNCTYPE(None, _c.c_int, _c.c_char_p, _c.c_void_p), _c.c_void_p] + lib.transcribe_model_accepts_ext_kind.restype = _c.c_bool + lib.transcribe_model_accepts_ext_kind.argtypes = [_c.c_void_p, _c.c_int, _c.c_uint32] + lib.transcribe_model_arch_string.restype = _c.c_char_p + lib.transcribe_model_arch_string.argtypes = [_c.c_void_p] + lib.transcribe_model_backend.restype = _c.c_char_p + lib.transcribe_model_backend.argtypes = [_c.c_void_p] + lib.transcribe_model_free.restype = None + lib.transcribe_model_free.argtypes = [_c.c_void_p] + lib.transcribe_model_get_capabilities.restype = _c.c_int + lib.transcribe_model_get_capabilities.argtypes = [_c.c_void_p, _c.POINTER(transcribe_capabilities)] + lib.transcribe_model_load_file.restype = _c.c_int + lib.transcribe_model_load_file.argtypes = [_c.c_char_p, _c.POINTER(transcribe_model_load_params), _c.POINTER(_c.c_void_p)] + lib.transcribe_model_load_params_init.restype = None + lib.transcribe_model_load_params_init.argtypes = [_c.POINTER(transcribe_model_load_params)] + lib.transcribe_model_supports.restype = _c.c_bool + lib.transcribe_model_supports.argtypes = [_c.c_void_p, _c.c_int] + lib.transcribe_model_variant_string.restype = _c.c_char_p + lib.transcribe_model_variant_string.argtypes = [_c.c_void_p] + lib.transcribe_moonshine_streaming_stream_ext_init.restype = None + lib.transcribe_moonshine_streaming_stream_ext_init.argtypes = [_c.POINTER(transcribe_moonshine_streaming_stream_ext)] + lib.transcribe_n_segments.restype = _c.c_int + lib.transcribe_n_segments.argtypes = [_c.c_void_p] + lib.transcribe_n_tokens.restype = _c.c_int + lib.transcribe_n_tokens.argtypes = [_c.c_void_p] + lib.transcribe_n_words.restype = _c.c_int + lib.transcribe_n_words.argtypes = [_c.c_void_p] + lib.transcribe_open.restype = _c.c_int + lib.transcribe_open.argtypes = [_c.c_char_p, _c.POINTER(transcribe_model_load_params), _c.POINTER(transcribe_session_params), _c.POINTER(_c.c_void_p)] + lib.transcribe_parakeet_buffered_stream_ext_init.restype = None + lib.transcribe_parakeet_buffered_stream_ext_init.argtypes = [_c.POINTER(transcribe_parakeet_buffered_stream_ext)] + lib.transcribe_parakeet_stream_ext_init.restype = None + lib.transcribe_parakeet_stream_ext_init.argtypes = [_c.POINTER(transcribe_parakeet_stream_ext)] + lib.transcribe_print_timings.restype = None + lib.transcribe_print_timings.argtypes = [_c.c_void_p] + lib.transcribe_reset_timings.restype = None + lib.transcribe_reset_timings.argtypes = [_c.c_void_p] + lib.transcribe_returned_timestamp_kind.restype = _c.c_int + lib.transcribe_returned_timestamp_kind.argtypes = [_c.c_void_p] + lib.transcribe_run.restype = _c.c_int + lib.transcribe_run.argtypes = [_c.c_void_p, _c.POINTER(_c.c_float), _c.c_int, _c.POINTER(transcribe_run_params)] + lib.transcribe_run_batch.restype = _c.c_int + lib.transcribe_run_batch.argtypes = [_c.c_void_p, _c.POINTER(_c.POINTER(_c.c_float)), _c.POINTER(_c.c_int), _c.c_int, _c.POINTER(transcribe_run_params)] + lib.transcribe_run_params_init.restype = None + lib.transcribe_run_params_init.argtypes = [_c.POINTER(transcribe_run_params)] + lib.transcribe_segment_init.restype = None + lib.transcribe_segment_init.argtypes = [_c.POINTER(transcribe_segment)] + lib.transcribe_session_free.restype = None + lib.transcribe_session_free.argtypes = [_c.c_void_p] + lib.transcribe_session_get_limits.restype = _c.c_int + lib.transcribe_session_get_limits.argtypes = [_c.c_void_p, _c.POINTER(transcribe_session_limits)] + lib.transcribe_session_init.restype = _c.c_int + lib.transcribe_session_init.argtypes = [_c.c_void_p, _c.POINTER(transcribe_session_params), _c.POINTER(_c.c_void_p)] + lib.transcribe_session_limits_init.restype = None + lib.transcribe_session_limits_init.argtypes = [_c.POINTER(transcribe_session_limits)] + lib.transcribe_session_params_init.restype = None + lib.transcribe_session_params_init.argtypes = [_c.POINTER(transcribe_session_params)] + lib.transcribe_set_abort_callback.restype = None + lib.transcribe_set_abort_callback.argtypes = [_c.c_void_p, _c.CFUNCTYPE(_c.c_bool, _c.c_void_p), _c.c_void_p] + lib.transcribe_status_string.restype = _c.c_char_p + lib.transcribe_status_string.argtypes = [_c.c_int] + lib.transcribe_stream_begin.restype = _c.c_int + lib.transcribe_stream_begin.argtypes = [_c.c_void_p, _c.POINTER(transcribe_run_params), _c.POINTER(transcribe_stream_params)] + lib.transcribe_stream_feed.restype = _c.c_int + lib.transcribe_stream_feed.argtypes = [_c.c_void_p, _c.POINTER(_c.c_float), _c.c_int, _c.POINTER(transcribe_stream_update)] + lib.transcribe_stream_finalize.restype = _c.c_int + lib.transcribe_stream_finalize.argtypes = [_c.c_void_p, _c.POINTER(transcribe_stream_update)] + lib.transcribe_stream_get_state.restype = _c.c_int + lib.transcribe_stream_get_state.argtypes = [_c.c_void_p] + lib.transcribe_stream_get_text.restype = _c.c_int + lib.transcribe_stream_get_text.argtypes = [_c.c_void_p, _c.POINTER(transcribe_stream_text)] + lib.transcribe_stream_last_status.restype = _c.c_int + lib.transcribe_stream_last_status.argtypes = [_c.c_void_p] + lib.transcribe_stream_n_committed_segments.restype = _c.c_int + lib.transcribe_stream_n_committed_segments.argtypes = [_c.c_void_p] + lib.transcribe_stream_n_committed_tokens.restype = _c.c_int + lib.transcribe_stream_n_committed_tokens.argtypes = [_c.c_void_p] + lib.transcribe_stream_n_committed_words.restype = _c.c_int + lib.transcribe_stream_n_committed_words.argtypes = [_c.c_void_p] + lib.transcribe_stream_params_init.restype = None + lib.transcribe_stream_params_init.argtypes = [_c.POINTER(transcribe_stream_params)] + lib.transcribe_stream_reset.restype = None + lib.transcribe_stream_reset.argtypes = [_c.c_void_p] + lib.transcribe_stream_revision.restype = _c.c_int + lib.transcribe_stream_revision.argtypes = [_c.c_void_p] + lib.transcribe_stream_text_init.restype = None + lib.transcribe_stream_text_init.argtypes = [_c.POINTER(transcribe_stream_text)] + lib.transcribe_stream_update_init.restype = None + lib.transcribe_stream_update_init.argtypes = [_c.POINTER(transcribe_stream_update)] + lib.transcribe_timings_init.restype = None + lib.transcribe_timings_init.argtypes = [_c.POINTER(transcribe_timings)] + lib.transcribe_token_init.restype = None + lib.transcribe_token_init.argtypes = [_c.POINTER(transcribe_token)] + lib.transcribe_tokenize.restype = _c.c_int + lib.transcribe_tokenize.argtypes = [_c.c_void_p, _c.c_char_p, _c.POINTER(_c.c_int32), _c.c_size_t] + lib.transcribe_version.restype = _c.c_char_p + lib.transcribe_version.argtypes = [] + lib.transcribe_version_commit.restype = _c.c_char_p + lib.transcribe_version_commit.argtypes = [] + lib.transcribe_voxtral_realtime_stream_ext_init.restype = None + lib.transcribe_voxtral_realtime_stream_ext_init.argtypes = [_c.POINTER(transcribe_voxtral_realtime_stream_ext)] + lib.transcribe_was_aborted.restype = _c.c_bool + lib.transcribe_was_aborted.argtypes = [_c.c_void_p] + lib.transcribe_was_truncated.restype = _c.c_bool + lib.transcribe_was_truncated.argtypes = [_c.c_void_p] + lib.transcribe_whisper_chunk_trace_init.restype = None + lib.transcribe_whisper_chunk_trace_init.argtypes = [_c.POINTER(transcribe_whisper_chunk_trace)] + lib.transcribe_whisper_run_ext_init.restype = None + lib.transcribe_whisper_run_ext_init.argtypes = [_c.POINTER(transcribe_whisper_run_ext)] + lib.transcribe_word_init.restype = None + lib.transcribe_word_init.argtypes = [_c.POINTER(transcribe_word)] diff --git a/bindings/python/src/transcribe_cpp/_library.py b/bindings/python/src/transcribe_cpp/_library.py new file mode 100644 index 00000000..ec6dc234 --- /dev/null +++ b/bindings/python/src/transcribe_cpp/_library.py @@ -0,0 +1,334 @@ +"""Locate and load the native ``transcribe`` shared library. + +Two worlds load the library, and this module is the single choke point for both: + +**Distribution (wheels).** The pure-Python API package carries no native code. +A *provider* package (``transcribe-cpp-native-cpu``, ``-metal``, ``-cu12``, …) +ships the artifact and advertises it through a Python *entry point* in group +``transcribe_cpp.native``. The entry point resolves to a zero-argument callable +returning a descriptor (a mapping or an object) with the contract fields: + + name provider distribution name (e.g. "transcribe-cpp-native-cpu") + library_path absolute path to the shared library to dlopen + (or artifact_dir + the platform filename) + artifact_dir directory holding the library and its sibling ggml libs + version native library version it was built for (base must match ours) + header_hash the _generated.PUBLIC_HEADER_HASH it was built against + backends supported backend kinds, e.g. ["cpu"] / ["metal", "cpu"] + +Selecting a provider picks which native artifact loads into the process; the +per-model ``backend=`` request is a *separate* axis resolved inside it. Selection +policy: explicit ``provider`` argument → ``TRANSCRIBE_NATIVE_PROVIDER`` env var → +best accelerated (CUDA/Metal, then Vulkan) → CPU. A discovered provider whose +declared version or header hash disagrees with this binding is a hard error +*before* dlopen — pip pins are not enough; this runtime check is the backstop. + +**Development (working tree).** No provider is installed; ``TRANSCRIBE_LIBRARY`` +points at a hand-built library, or one is discovered under the repo's +``build-shared/`` / ``build/`` tree. This path skips the provider contract (the +developer owns the build); the import-time ABI layout check in ``_abi`` is the +correctness backstop either way. +""" + +from __future__ import annotations + +import ctypes +import importlib.metadata +import os +import sys +from pathlib import Path +from typing import Callable, Iterator, Optional + +from . import _generated +from .errors import TranscribeError + +#: Entry-point group a native provider package registers under. +ENTRY_POINT_GROUP = "transcribe_cpp.native" + +#: Backend-kind preference when auto-selecting among installed providers. Higher +#: wins; a provider's rank is the max over the kinds it advertises. +_BACKEND_RANK = {"cuda": 3, "metal": 3, "vulkan": 2, "cpu_accel": 1, "cpu": 1} + +#: Set after a successful load so the package can surface it for diagnostics. +#: None means the library came from the dev-tree / explicit-path fallback. +_selected_provider: Optional[str] = None + + +def selected_provider() -> Optional[str]: + """Name of the provider package the loaded library came from, or None for a + dev-tree / ``TRANSCRIBE_LIBRARY`` load.""" + return _selected_provider + + +def _library_filename() -> str: + if sys.platform == "darwin": + return "libtranscribe.dylib" + if sys.platform == "win32": + return "transcribe.dll" + return "libtranscribe.so" + + +def _base_version(version: str) -> str: + """Leading dotted-numeric release segment, suffix stripped (PEP 440).""" + import re + + m = re.match(r"\d+(?:\.\d+)*", version.strip()) + return m.group(0) if m else version.strip() + + +# --- provider discovery --------------------------------------------------- + + +def _descriptor_field(descriptor, key: str): + """Read a contract field from a mapping- or attribute-style descriptor.""" + if isinstance(descriptor, dict): + return descriptor.get(key) + return getattr(descriptor, key, None) + + +def _iter_native_entry_points(): + """Entry points in ENTRY_POINT_GROUP, across the 3.9 vs 3.10+ APIs.""" + eps = importlib.metadata.entry_points() + if hasattr(eps, "select"): # 3.10+ + return list(eps.select(group=ENTRY_POINT_GROUP)) + return list(eps.get(ENTRY_POINT_GROUP, [])) # 3.9 + + +class _Provider: + """A discovered, normalized provider descriptor.""" + + def __init__(self, ep_name: str, descriptor): + self.ep_name = ep_name + self.name = _descriptor_field(descriptor, "name") or ep_name + self.version = _descriptor_field(descriptor, "version") + self.header_hash = _descriptor_field(descriptor, "header_hash") + self.backends = tuple(_descriptor_field(descriptor, "backends") or ()) + self._library_path = _descriptor_field(descriptor, "library_path") + self._artifact_dir = _descriptor_field(descriptor, "artifact_dir") + + @property + def rank(self) -> int: + return max((_BACKEND_RANK.get(b, 0) for b in self.backends), default=0) + + def matches_request(self, request: str) -> bool: + """Whether an explicit provider request names this provider — by dist + name, by an advertised backend kind, or by the conventional + ``…-native-`` suffix.""" + request = request.strip().lower() + name = (self.name or "").lower() + return ( + request == name + or request == self.ep_name.lower() + or request in {b.lower() for b in self.backends} + or name.endswith(f"-{request}") + or name.endswith(f"-native-{request}") + ) + + def library_path(self) -> Path: + if self._library_path: + return Path(self._library_path) + if self._artifact_dir: + return Path(self._artifact_dir) / _library_filename() + raise TranscribeError( + message=( + f"native provider {self.name!r} supplied neither 'library_path' " + "nor 'artifact_dir' in its entry-point descriptor" + ) + ) + + def validate_contract(self) -> None: + """Hard-fail if the provider disagrees with this binding on ABI or + version. Raises TranscribeError with an actionable message.""" + expected_hash = _generated.PUBLIC_HEADER_HASH + if self.header_hash and self.header_hash != expected_hash: + raise TranscribeError( + message=( + f"native provider {self.name!r} was built against a different " + f"public ABI (header hash {self.header_hash!r}, this binding " + f"expects {expected_hash!r}). Install a provider matching " + "transcribe-cpp, or upgrade/downgrade transcribe-cpp to match " + "the provider." + ) + ) + api_base = _api_version_base() + if self.version and api_base and _base_version(self.version) != api_base: + raise TranscribeError( + message=( + f"native provider {self.name!r} is version {self.version}, but " + f"transcribe-cpp is {api_base}.x: pre-1.0 requires a matching " + "base (MAJOR.MINOR.PATCH). Install matching versions of " + "transcribe-cpp and its native provider." + ) + ) + + +def _api_version_base() -> Optional[str]: + """Base version of the installed API distribution, or None if not installed + as a distribution (a pure source/PYTHONPATH run, where no provider exists).""" + try: + return _base_version(importlib.metadata.version("transcribe-cpp")) + except importlib.metadata.PackageNotFoundError: + return None + + +def _discover_providers() -> list: + providers = [] + for ep in _iter_native_entry_points(): + try: + factory: Callable = ep.load() + descriptor = factory() if callable(factory) else factory + providers.append(_Provider(ep.name, descriptor)) + except Exception as exc: # a broken provider must not hide the others + providers.append(_BrokenProvider(ep.name, exc)) + return providers + + +class _BrokenProvider: + """A provider whose entry point failed to load — kept so selection can name + it in diagnostics rather than silently dropping it.""" + + rank = -1 + backends: tuple = () + + def __init__(self, ep_name: str, error: Exception): + self.ep_name = ep_name + self.name = ep_name + self.error = error + + def matches_request(self, request: str) -> bool: + return request.strip().lower() in (self.ep_name.lower(), self.name.lower()) + + +def _select_provider(providers: list, request: Optional[str]) -> Optional["_Provider"]: + """Apply the selection policy. Returns the chosen provider, or None when no + providers are installed. Raises when an explicit request cannot be honored.""" + healthy = [p for p in providers if isinstance(p, _Provider)] + + if request: + for p in providers: + if p.matches_request(request): + if isinstance(p, _BrokenProvider): + raise TranscribeError( + message=( + f"requested native provider {request!r} failed to " + f"load: {p.error}" + ) + ) + return p + installed = ", ".join(sorted(p.name for p in providers)) or "(none)" + raise TranscribeError( + message=( + f"requested native provider {request!r} is not installed. " + f"Installed providers: {installed}. Install it, e.g. " + f'pip install "transcribe-cpp[{request}]".' + ) + ) + + if not providers: + return None + if not healthy: + errors = "; ".join(f"{p.name}: {p.error}" for p in providers) + raise TranscribeError( + message=f"every installed native provider failed to load: {errors}" + ) + # Best accelerated first (CUDA/Metal, then Vulkan, then CPU); deterministic + # tie-break by name so selection is stable across runs. + healthy.sort(key=lambda p: (-p.rank, p.name)) + return healthy[0] + + +# --- developer / bundled fallback candidates ------------------------------ + + +def _ascend_to_repo_root(start: Path) -> Optional[Path]: + """Walk up from *start* to the transcribe.cpp repo root, if we are in one.""" + for parent in (start, *start.parents): + if (parent / "CMakeLists.txt").is_file() and ( + parent / "include" / "transcribe.h" + ).is_file(): + return parent + return None + + +def _fallback_candidate_paths() -> Iterator[Path]: + """In-package bundled artifact, then dev-tree build outputs.""" + name = _library_filename() + + pkg_dir = Path(__file__).resolve().parent + yield pkg_dir / "_native" / name + yield pkg_dir / name + + repo_root = _ascend_to_repo_root(pkg_dir) + if repo_root is not None: + for build_dir in ("build-shared", "build"): + yield repo_root / build_dir / "src" / name + yield repo_root / build_dir / "bin" / name + + +def _cdll(path: Path) -> ctypes.CDLL: + # Windows resolves sibling DLLs (ggml, …) via the DLL search path, not + # rpath; add the library's own directory before loading it. + if sys.platform == "win32": + os.add_dll_directory(str(path.parent)) + return ctypes.CDLL(str(path)) + + +def load_library(provider: Optional[str] = None) -> tuple: + """Return ``(CDLL, path)`` for the native library, or raise TranscribeError. + + Resolution order: + + 1. ``TRANSCRIBE_LIBRARY`` — explicit path (developer escape hatch). + 2. An installed provider package (entry-point discovery + contract check). + 3. A bundled in-package ``_native/`` artifact, then a dev-tree build. + + *provider* (or ``TRANSCRIBE_NATIVE_PROVIDER``) forces a specific installed + provider; otherwise the best accelerated one is chosen, falling back to CPU. + """ + global _selected_provider + _selected_provider = None + + # 1. Explicit path override — bypasses provider discovery entirely. + override = os.environ.get("TRANSCRIBE_LIBRARY") + if override: + path = Path(override) + if not path.is_file(): + raise TranscribeError( + message=f"TRANSCRIBE_LIBRARY points at a missing file: {path}" + ) + return _cdll(path), path + + # 2. Installed provider packages. + request = provider or os.environ.get("TRANSCRIBE_NATIVE_PROVIDER") + providers = _discover_providers() + chosen = _select_provider(providers, request) + if chosen is not None: + chosen.validate_contract() + path = chosen.library_path() + if not path.is_file(): + raise TranscribeError( + message=( + f"native provider {chosen.name!r} points at a missing " + f"library: {path}" + ) + ) + cdll = _cdll(path) + _selected_provider = chosen.name + return cdll, path + + # 3. Bundled / dev-tree fallback. + tried: list = [] + for path in _fallback_candidate_paths(): + tried.append(str(path)) + if path.is_file(): + return _cdll(path), path + + raise TranscribeError( + message=( + "could not locate the native transcribe library. Install a native " + 'provider (e.g. pip install "transcribe-cpp[cpu]"), set ' + "TRANSCRIBE_LIBRARY to a built library, or build a shared library " + "(cmake -DTRANSCRIBE_BUILD_SHARED=ON). Searched:\n " + + "\n ".join(tried) + ) + ) diff --git a/bindings/python/src/transcribe_cpp/errors.py b/bindings/python/src/transcribe_cpp/errors.py new file mode 100644 index 00000000..b8a00613 --- /dev/null +++ b/bindings/python/src/transcribe_cpp/errors.py @@ -0,0 +1,113 @@ +"""Exception hierarchy for transcribe.cpp, mapped from ``transcribe_status``. + +Status numbers mirror the ``transcribe_status`` enum in +``include/transcribe.h``. The mapping is intentionally explicit (not derived +from the native enum) so a binding/library version skew surfaces as a clear +Python error rather than a wrong subclass. +""" + +from __future__ import annotations + +# transcribe_status values (include/transcribe.h). +OK = 0 +ERR_INVALID_ARG = 1 +ERR_NOT_IMPLEMENTED = 2 +ERR_FILE_NOT_FOUND = 3 +ERR_GGUF = 4 +ERR_UNSUPPORTED_ARCH = 5 +ERR_UNSUPPORTED_VARIANT = 6 +ERR_OOM = 7 +ERR_BACKEND = 8 +ERR_SAMPLE_RATE = 9 +ERR_UNSUPPORTED_LANGUAGE = 10 +ERR_UNSUPPORTED_TASK = 11 +ERR_UNSUPPORTED_TIMESTAMPS = 12 +ERR_ABORTED = 13 +ERR_BAD_STRUCT_SIZE = 14 +ERR_UNSUPPORTED_PNC = 15 +ERR_UNSUPPORTED_ITN = 16 +ERR_INPUT_TOO_LONG = 17 +ERR_OUTPUT_TRUNCATED = 18 + + +class TranscribeError(RuntimeError): + """Base class for every transcribe.cpp error. + + ``status`` is the numeric ``transcribe_status`` (0 for errors raised purely + on the Python side, e.g. library loading or input validation). + """ + + def __init__(self, message: str, status: int = OK): + super().__init__(message) + self.status = status + + +class InvalidArgument(TranscribeError): + pass + + +class NotImplementedByModel(TranscribeError): + pass + + +class ModelFileNotFound(TranscribeError): + pass + + +class ModelLoadError(TranscribeError): + """GGUF parse / unsupported arch / unsupported variant.""" + + +class OutOfMemory(TranscribeError): + pass + + +class BackendError(TranscribeError): + pass + + +class UnsupportedRequest(TranscribeError): + """Task / language / timestamp granularity the model does not support.""" + + +class AbiError(TranscribeError): + """Caller-owned struct layout did not match the library (struct_size).""" + + +class InputTooLong(TranscribeError): + pass + + +class OutputTruncated(TranscribeError): + pass + + +_STATUS_TO_EXC = { + ERR_INVALID_ARG: InvalidArgument, + ERR_NOT_IMPLEMENTED: NotImplementedByModel, + ERR_FILE_NOT_FOUND: ModelFileNotFound, + ERR_GGUF: ModelLoadError, + ERR_UNSUPPORTED_ARCH: ModelLoadError, + ERR_UNSUPPORTED_VARIANT: ModelLoadError, + ERR_OOM: OutOfMemory, + ERR_BACKEND: BackendError, + ERR_SAMPLE_RATE: InvalidArgument, + ERR_UNSUPPORTED_LANGUAGE: UnsupportedRequest, + ERR_UNSUPPORTED_TASK: UnsupportedRequest, + ERR_UNSUPPORTED_TIMESTAMPS: UnsupportedRequest, + ERR_ABORTED: TranscribeError, + ERR_BAD_STRUCT_SIZE: AbiError, + ERR_UNSUPPORTED_PNC: UnsupportedRequest, + ERR_UNSUPPORTED_ITN: UnsupportedRequest, + ERR_INPUT_TOO_LONG: InputTooLong, + ERR_OUTPUT_TRUNCATED: OutputTruncated, +} + + +def raise_for_status(status: int, status_string: str, context: str = "") -> None: + """Raise the mapped exception for a non-OK status; return on OK.""" + if status == OK: + return + exc = _STATUS_TO_EXC.get(status, TranscribeError) + prefix = f"{context}: " if context else "" + raise exc(f"{prefix}{status_string} (status {status})", status=status) diff --git a/bindings/python/src/transcribe_cpp/py.typed b/bindings/python/src/transcribe_cpp/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py new file mode 100644 index 00000000..f2716e9f --- /dev/null +++ b/bindings/python/tests/conftest.py @@ -0,0 +1,88 @@ +"""Shared fixtures for the transcribe_cpp pytest suite. + +Two tiers of tests: + + - **No-model tests** (test_abi.py) need only an importable package, which + means a loadable native library. Importing ``transcribe_cpp`` already runs + the loader, the ABI layout check, and the version gate; these tests pin that + surface explicitly. They always run in CI's shared-build lane. + + - **Model tests** (test_transcribe.py, test_streaming.py) need a real GGUF and + audio on disk. They ``skip`` (never fail) when the asset is absent, so the + suite is honest on a machine without the multi-GB checkpoints. + +Asset resolution mirrors the old stdlib smoke runner: a default +whisper-tiny.en + samples/jfk.wav in the repo, each overridable by +``TRANSCRIBE_SMOKE_MODEL`` / ``TRANSCRIBE_SMOKE_AUDIO``. +""" + +from __future__ import annotations + +import array +import os +import sys +import wave +from pathlib import Path + +import pytest + +# tests/ -> python/ -> bindings/ -> repo root. +REPO = Path(__file__).resolve().parents[3] +SAMPLES = REPO / "samples" + +DEFAULT_MODEL = REPO / "models/whisper-tiny.en/whisper-tiny.en-Q5_K_M.gguf" +DEFAULT_AUDIO = SAMPLES / "jfk.wav" +STREAMING_MODEL = ( + REPO / "models/moonshine-streaming-tiny/moonshine-streaming-tiny-Q8_0.gguf" +) + + +def load_wav(path: Path) -> "array.array": + """Read a 16 kHz mono 16-bit WAV into a float32 ``array`` in [-1, 1).""" + with wave.open(str(path), "rb") as w: + assert w.getsampwidth() == 2 and w.getframerate() == 16000, ( + f"{path} must be 16 kHz 16-bit mono" + ) + pcm16 = array.array("h") + pcm16.frombytes(w.readframes(w.getnframes())) + if sys.byteorder == "big": + pcm16.byteswap() + return array.array("f", (s / 32768.0 for s in pcm16)) + + +@pytest.fixture(scope="session") +def transcribe_cpp(): + """The imported package (importing it exercises load + ABI + version gate).""" + import transcribe_cpp + + return transcribe_cpp + + +@pytest.fixture(scope="session") +def model_path() -> Path: + override = os.environ.get("TRANSCRIBE_SMOKE_MODEL") + path = Path(override) if override else DEFAULT_MODEL + if not path.is_file(): + pytest.skip(f"model not present: {path} (set TRANSCRIBE_SMOKE_MODEL)") + return path + + +@pytest.fixture(scope="session") +def audio_path() -> Path: + override = os.environ.get("TRANSCRIBE_SMOKE_AUDIO") + path = Path(override) if override else DEFAULT_AUDIO + if not path.is_file(): + pytest.skip(f"audio not present: {path} (set TRANSCRIBE_SMOKE_AUDIO)") + return path + + +@pytest.fixture(scope="session") +def audio_pcm(audio_path: Path) -> "array.array": + return load_wav(audio_path) + + +@pytest.fixture(scope="session") +def streaming_model_path() -> Path: + if not STREAMING_MODEL.is_file(): + pytest.skip(f"streaming model not present: {STREAMING_MODEL}") + return STREAMING_MODEL diff --git a/bindings/python/tests/test_abi.py b/bindings/python/tests/test_abi.py new file mode 100644 index 00000000..2e939db6 --- /dev/null +++ b/bindings/python/tests/test_abi.py @@ -0,0 +1,94 @@ +"""No-model tests: ABI layout, version gate, and status-code/enum agreement. + +These run anywhere the native library loads — no GGUF required. Importing +``transcribe_cpp`` already ran the loader, ``_abi.verify_layouts``, and the +version gate; here we pin each surface explicitly so a regression names itself. +""" + +from __future__ import annotations + +import pytest + +import transcribe_cpp as t +from transcribe_cpp import _abi, _generated, errors + + +def test_native_version_matches_binding(): + # The import-time gate compares *base* versions; assert the same here so a + # base mismatch is a named failure rather than a bare ImportError. + assert t._base_version(t.native_version()) == t._base_version(t.__version__) + + +def test_abi_layouts_reverify(): + # Idempotent: import did this once; a second pass must still agree. + _abi.verify_layouts(t._lib) + + +def test_every_abi_struct_known_to_native(): + assert _generated.ABI_STRUCT_IDS, "no ABI struct ids generated" + for name, abi_id in _generated.ABI_STRUCT_IDS.items(): + assert t._lib.transcribe_abi_struct_size(abi_id) > 0, name + + +def test_struct_layout_offsets_present(): + # A guard against an empty/garbage generated layout silently passing. + n_fields = sum(len(lo["offsets"]) for lo in _generated.STRUCT_LAYOUT.values()) + assert len(_generated.STRUCT_LAYOUT) >= 10 + assert n_fields >= 50 + + +@pytest.mark.parametrize( + "version,base", + [ + ("0.0.1", "0.0.1"), + ("0.0.1.post1", "0.0.1"), + ("0.0.1.post42", "0.0.1"), + ("0.1.0.dev3", "0.1.0"), + ("1.2.3rc1", "1.2.3"), + ("1.2.3a4", "1.2.3"), + ("0.0.1+local.build", "0.0.1"), + ("2.0.0", "2.0.0"), + ], +) +def test_base_version_strips_suffixes(version, base): + assert t._base_version(version) == base + + +# --- status-code <-> generated-enum agreement (M2) ------------------------ +# +# errors.py mirrors the transcribe_status enum by hand (so a skew surfaces as a +# clear Python error, not a wrong subclass). These tests pin that the hand-kept +# numbers still equal the generated enum, in both directions, so a native status +# code that the Python layer forgot to mirror or map cannot slip through. + +_STATUS_NAMES = [n for n in dir(errors) if n == "OK" or n.startswith("ERR_")] + + +def test_status_constants_discovered(): + # transcribe_status has OK + 18 error codes as of this writing; never let + # this collapse to a near-empty set that would make the checks vacuous. + assert len(_STATUS_NAMES) >= 19 + + +@pytest.mark.parametrize("name", _STATUS_NAMES) +def test_status_code_matches_generated(name): + generated_name = "TRANSCRIBE_" + name + assert hasattr(_generated, generated_name), ( + f"{generated_name} missing from generated enum" + ) + assert getattr(errors, name) == getattr(_generated, generated_name) + + +def test_every_generated_status_is_mirrored_and_mapped(): + # Reverse direction: every TRANSCRIBE_OK / TRANSCRIBE_ERR_* the native + # library exposes must have a Python constant AND (for errors) a mapped + # exception, so adding a code natively fails CI until Python catches up. + py_values = {getattr(errors, n) for n in _STATUS_NAMES} + for gen_name in dir(_generated): + if gen_name == "TRANSCRIBE_OK" or gen_name.startswith("TRANSCRIBE_ERR_"): + value = getattr(_generated, gen_name) + assert value in py_values, f"{gen_name} not mirrored in errors.py" + if gen_name != "TRANSCRIBE_OK": + assert value in errors._STATUS_TO_EXC, ( + f"{gen_name} has no mapped exception in errors._STATUS_TO_EXC" + ) diff --git a/bindings/python/tests/test_provider_discovery.py b/bindings/python/tests/test_provider_discovery.py new file mode 100644 index 00000000..56d8f7a2 --- /dev/null +++ b/bindings/python/tests/test_provider_discovery.py @@ -0,0 +1,168 @@ +"""Unit tests for native-provider discovery and selection (_library). + +No real provider package is installed in CI, so these drive the selection and +contract machinery with synthetic descriptors — the logic that decides which +artifact loads and rejects an ABI/version-mismatched provider before dlopen. +""" + +from __future__ import annotations + +import pytest + +import transcribe_cpp as t +from transcribe_cpp import _generated, _library +from transcribe_cpp.errors import TranscribeError + +GOOD_HASH = _generated.PUBLIC_HEADER_HASH +API_BASE = _library._api_version_base() + + +def make(name, backends, *, version=None, header_hash=GOOD_HASH, + library_path="/x/libtranscribe.so", artifact_dir=None): + return _library._Provider( + name, + { + "name": name, + "backends": backends, + "version": version if version is not None else (API_BASE or "0.0.1"), + "header_hash": header_hash, + "library_path": library_path, + "artifact_dir": artifact_dir, + }, + ) + + +# --- descriptor normalization & matching ---------------------------------- + + +def test_descriptor_from_mapping_and_object(): + class Obj: + name = "transcribe-cpp-native-cpu" + backends = ["cpu"] + version = "0.0.1" + header_hash = GOOD_HASH + library_path = "/x/libtranscribe.so" + artifact_dir = None + + p = _library._Provider("cpu", Obj()) + assert p.name == "transcribe-cpp-native-cpu" + assert p.backends == ("cpu",) + assert p.library_path().name.startswith("libtranscribe") + + +def test_library_path_from_artifact_dir(): + p = make("transcribe-cpp-native-cpu", ["cpu"], + library_path=None, artifact_dir="/opt/art") + assert str(p.library_path()).startswith("/opt/art") + + +def test_library_path_requires_a_source(): + p = make("x", ["cpu"], library_path=None, artifact_dir=None) + with pytest.raises(TranscribeError): + p.library_path() + + +@pytest.mark.parametrize( + "request_str,expected", + [ + ("transcribe-cpp-native-cpu", True), + ("cpu", True), # backend kind and name suffix + ("metal", False), + ("transcribe-cpp-native-metal", False), + ], +) +def test_matches_request(request_str, expected): + p = make("transcribe-cpp-native-cpu", ["cpu"]) + assert p.matches_request(request_str) is expected + + +def test_rank_orders_accelerated_above_cpu(): + assert make("m", ["metal", "cpu"]).rank == 3 + assert make("v", ["vulkan"]).rank == 2 + assert make("c", ["cpu"]).rank == 1 + assert make("u", ["wat"]).rank == 0 + + +# --- selection policy ------------------------------------------------------ + + +def test_select_prefers_accelerated(): + cpu = make("transcribe-cpp-native-cpu", ["cpu"]) + metal = make("transcribe-cpp-native-metal", ["metal", "cpu"]) + assert _library._select_provider([cpu, metal], None) is metal + + +def test_select_is_deterministic_on_ties(): + a = make("transcribe-cpp-native-cpu", ["cpu"]) + b = make("transcribe-cpp-native-cpu2", ["cpu"]) + # Same rank → alphabetical by name, stable regardless of input order. + assert _library._select_provider([a, b], None).name == "transcribe-cpp-native-cpu" + assert _library._select_provider([b, a], None).name == "transcribe-cpp-native-cpu" + + +def test_select_explicit_request(): + cpu = make("transcribe-cpp-native-cpu", ["cpu"]) + metal = make("transcribe-cpp-native-metal", ["metal", "cpu"]) + assert _library._select_provider([cpu, metal], "cpu") is cpu + + +def test_select_none_when_no_providers(): + assert _library._select_provider([], None) is None + + +def test_select_unknown_request_raises(): + cpu = make("transcribe-cpp-native-cpu", ["cpu"]) + with pytest.raises(TranscribeError, match="not installed"): + _library._select_provider([cpu], "vulkan") + + +def test_select_broken_only_raises(): + broken = _library._BrokenProvider("transcribe-cpp-native-cpu", RuntimeError("boom")) + with pytest.raises(TranscribeError, match="failed to load"): + _library._select_provider([broken], None) + + +def test_select_request_for_broken_raises(): + broken = _library._BrokenProvider("transcribe-cpp-native-cpu", RuntimeError("boom")) + with pytest.raises(TranscribeError, match="failed to load"): + _library._select_provider([broken], "transcribe-cpp-native-cpu") + + +# --- contract validation --------------------------------------------------- + + +def test_contract_ok_for_matching_provider(): + make("transcribe-cpp-native-cpu", ["cpu"]).validate_contract() # no raise + + +def test_contract_rejects_header_hash_mismatch(): + p = make("transcribe-cpp-native-cpu", ["cpu"], header_hash="deadbeefdeadbeef") + with pytest.raises(TranscribeError, match="different\\s+public ABI"): + p.validate_contract() + + +def test_contract_skips_hash_check_when_provider_omits_it(): + # A provider that declares no header hash is trusted on ABI (the import-time + # struct-layout check in _abi is still the deep backstop). + make("transcribe-cpp-native-cpu", ["cpu"], header_hash=None).validate_contract() + + +@pytest.mark.skipif(API_BASE is None, reason="API distribution not installed") +def test_contract_rejects_version_base_mismatch(): + major = int(API_BASE.split(".")[0]) + bumped = f"{major + 1}.0.0" + p = make("transcribe-cpp-native-cpu", ["cpu"], version=bumped) + with pytest.raises(TranscribeError, match="requires a matching"): + p.validate_contract() + + +@pytest.mark.skipif(API_BASE is None, reason="API distribution not installed") +def test_contract_accepts_post_release_provider(): + # A .postN packaging fix on the provider keeps the same base and must load. + p = make("transcribe-cpp-native-cpu", ["cpu"], version=f"{API_BASE}.post3") + p.validate_contract() # no raise + + +def test_no_provider_installed_in_test_env(): + # Sanity: this binding ran from the dev tree, not a provider package. + assert t.native_provider() is None diff --git a/bindings/python/tests/test_streaming.py b/bindings/python/tests/test_streaming.py new file mode 100644 index 00000000..6b5fe295 --- /dev/null +++ b/bindings/python/tests/test_streaming.py @@ -0,0 +1,48 @@ +"""Streaming + cancellation tests. + +The streaming *gate* runs against the default (non-streaming) whisper model and +needs no streaming checkpoint. The real streaming and cancellation tests need +moonshine-streaming-tiny and ``skip`` when it is absent. +""" + +from __future__ import annotations + +import pytest + +import transcribe_cpp as t + + +def test_streaming_gate(model_path): + with t.Model(model_path) as model: + if model.capabilities.supports_streaming: + pytest.skip("default model supports streaming; gate not exercised") + with model.session() as session: + with pytest.raises(t.NotImplementedByModel): + session.stream() + + +def test_streaming_real(streaming_model_path, audio_pcm): + with t.Model(streaming_model_path) as model: + assert model.capabilities.supports_streaming + with model.session() as session, session.stream() as stream: + for i in range(0, len(audio_pcm), 16000): # ~1s chunks + stream.feed(audio_pcm[i : i + 16000]) + update = stream.finalize() + committed = stream.text().committed + revision, state = stream.revision, stream.state + assert update.is_final, update + assert state == "finished", state + assert revision >= 1 + assert "country" in committed.lower(), committed + + +def test_cancellation_aborts_in_flight_feed(streaming_model_path, audio_pcm): + with t.Model(streaming_model_path) as model, model.session() as session: + with session.stream() as stream: + session.cancel() # request abort, then feed: must abort in-flight + try: + stream.feed(audio_pcm) + except t.TranscribeError: + assert session.was_aborted, "was_aborted should be True after cancel()" + return + raise AssertionError("cancel() did not abort the feed") diff --git a/bindings/python/tests/test_transcribe.py b/bindings/python/tests/test_transcribe.py new file mode 100644 index 00000000..9c822fed --- /dev/null +++ b/bindings/python/tests/test_transcribe.py @@ -0,0 +1,66 @@ +"""Model-gated transcription tests: run, GIL release, batch, logging. + +Each takes the ``model_path`` / ``audio_pcm`` fixtures, which ``skip`` when the +default whisper-tiny.en + jfk.wav assets are absent (override with +``TRANSCRIBE_SMOKE_MODEL`` / ``TRANSCRIBE_SMOKE_AUDIO``). The content assertions +("country") are specific to jfk.wav and hold for any English ASR model. +""" + +from __future__ import annotations + +import threading +import time + +import transcribe_cpp as t + + +def test_transcription_and_gil(model_path, audio_pcm): + # GIL-release probe: a worker spins in pure Python (GIL-bound). If the native + # run releases the GIL, the worker advances while transcription is in flight. + counter = {"n": 0} + stop = threading.Event() + + def spin(): + while not stop.is_set(): + counter["n"] += 1 + + worker = threading.Thread(target=spin, daemon=True) + worker.start() + time.sleep(0.01) + try: + with t.Model(model_path) as model: + assert model.arch, "model reported an empty arch string" + with model.session() as session: + before = counter["n"] + result = session.run(audio_pcm, timestamps="segment") + during = counter["n"] - before + finally: + stop.set() + worker.join(timeout=1.0) + + text = result.text.strip().lower() + assert text, "empty transcription" + assert "country" in text, f"unexpected transcription: {result.text!r}" + assert result.segments, "no segments materialized" + assert during > 100, f"GIL not released during run (worker advanced {during})" + + +def test_run_batch(model_path, audio_pcm): + with t.Model(model_path) as model, model.session() as session: + results = session.run_batch([audio_pcm, audio_pcm], timestamps="segment") + assert len(results) == 2 + assert all("country" in r.text.lower() for r in results), results + assert all(r.segments for r in results) + + +def test_logging_routes_through_public_sink(model_path, audio_pcm): + received = [] + t.set_log_callback(lambda level, msg: received.append((level, msg))) + try: + with t.Model(model_path) as model, model.session() as session: + session.run(audio_pcm) + # print_timings publishes through the public log sink at INFO level. + t._lib.transcribe_print_timings(session._h) + assert received, "log callback received nothing from the public sink" + finally: + t.set_log_callback(None) diff --git a/bindings/python/uv.lock b/bindings/python/uv.lock new file mode 100644 index 00000000..f3d12110 --- /dev/null +++ b/bindings/python/uv.lock @@ -0,0 +1,197 @@ +version = 1 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704 }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454 }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561 }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824 }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227 }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859 }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204 }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084 }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285 }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924 }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018 }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948 }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341 }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159 }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290 }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141 }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847 }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088 }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866 }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887 }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704 }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628 }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180 }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674 }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976 }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755 }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265 }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726 }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859 }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713 }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084 }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973 }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223 }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973 }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082 }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490 }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263 }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736 }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717 }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461 }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855 }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144 }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683 }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196 }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393 }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583 }, +] + +[[package]] +name = "transcribe-cpp" +version = "0.0.1" +source = { editable = "." } + +[package.optional-dependencies] +test = [ + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [{ name = "pytest", marker = "extra == 'test'", specifier = ">=7" }] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] diff --git a/include/transcribe.h b/include/transcribe.h index 4412b9c9..8bca3c89 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -132,6 +132,34 @@ #include #include +/* ----------------------------------------------------------------------- */ +/* Version */ +/* ----------------------------------------------------------------------- */ +/* + * Single source of truth for the native library version. The top-level + * CMakeLists.txt parses these three macros to set the CMake project version and + * the shared-library VERSION/SOVERSION, and transcribe_version() (declared + * below) returns the same MAJOR.MINOR.PATCH string. Bump the library version + * here. Pre-1.0 the on-disk ABI MAY break between minor releases (see "ABI + * stability" above); the Python binding pins its package version to this value + * exactly and refuses to load a native provider that does not match. + */ +#define TRANSCRIBE_VERSION_MAJOR 0 +#define TRANSCRIBE_VERSION_MINOR 0 +#define TRANSCRIBE_VERSION_PATCH 1 + +#define TRANSCRIBE_VERSION_STRINGIZE_(x) #x +#define TRANSCRIBE_VERSION_STRINGIZE(x) TRANSCRIBE_VERSION_STRINGIZE_(x) +#define TRANSCRIBE_VERSION \ + TRANSCRIBE_VERSION_STRINGIZE(TRANSCRIBE_VERSION_MAJOR) "." \ + TRANSCRIBE_VERSION_STRINGIZE(TRANSCRIBE_VERSION_MINOR) "." \ + TRANSCRIBE_VERSION_STRINGIZE(TRANSCRIBE_VERSION_PATCH) + +/* Monotonic integer form (MAJOR*10000 + MINOR*100 + PATCH) for compile-time + * comparisons, e.g. `#if TRANSCRIBE_VERSION_NUMBER >= 200`. */ +#define TRANSCRIBE_VERSION_NUMBER \ + (TRANSCRIBE_VERSION_MAJOR * 10000 + TRANSCRIBE_VERSION_MINOR * 100 + TRANSCRIBE_VERSION_PATCH) + #ifndef TRANSCRIBE_API # if defined(_WIN32) && !defined(__GNUC__) # ifdef TRANSCRIBE_BUILD @@ -288,6 +316,63 @@ typedef enum { */ TRANSCRIBE_API const char * transcribe_status_string(int status); +/* ----------------------------------------------------------------------- */ +/* Version */ +/* ----------------------------------------------------------------------- */ +/* + * Runtime version of the loaded native library. The numeric components are + * also available at compile time as TRANSCRIBE_VERSION_MAJOR/MINOR/PATCH (and + * the composed string as TRANSCRIBE_VERSION) near the top of this header. + * + * Both return borrowed pointers into static storage: never free them, and + * treat them as valid for the life of the process. + */ +/* "MAJOR.MINOR.PATCH", e.g. "0.1.0". Equals the TRANSCRIBE_VERSION macro the + * caller compiled against; a mismatch means the header and the linked library + * disagree. */ +TRANSCRIBE_API const char * transcribe_version(void); +/* Short git commit the library was built from, or "unknown" when the build + * tree carried no git metadata (e.g. an unpacked source tarball). */ +TRANSCRIBE_API const char * transcribe_version_commit(void); + +/* ----------------------------------------------------------------------- */ +/* ABI metadata */ +/* ----------------------------------------------------------------------- */ +/* + * The native library's sizeof/alignof for the public structs that cross the + * ABI. A binding (Python ctypes, Rust, Swift, ...) declares its own view of + * each struct, then verifies that view against these values before it + * constructs any real instance. This is the safe alternative the "Params" + * section calls for: query the size up front instead of calling a *_init() + * function on a buffer that might be smaller than the library expects. + * + * `which` selects a struct by transcribe_abi_struct. An unknown id (e.g. a + * newer binding asking an older library about a struct it predates) returns 0, + * which the binding MUST treat as "cannot verify," never as "size 0." The enum + * is append-only; do not renumber existing values. + */ +typedef enum { + TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0, + TRANSCRIBE_ABI_SESSION_PARAMS = 1, + TRANSCRIBE_ABI_RUN_PARAMS = 2, + TRANSCRIBE_ABI_STREAM_PARAMS = 3, + TRANSCRIBE_ABI_CAPABILITIES = 4, + TRANSCRIBE_ABI_TIMINGS = 5, + TRANSCRIBE_ABI_SEGMENT = 6, + TRANSCRIBE_ABI_WORD = 7, + TRANSCRIBE_ABI_TOKEN = 8, + TRANSCRIBE_ABI_STREAM_UPDATE = 9, + TRANSCRIBE_ABI_STREAM_TEXT = 10, + TRANSCRIBE_ABI_SESSION_LIMITS = 11, + TRANSCRIBE_ABI_EXT = 12, +} transcribe_abi_struct; + +/* sizeof / alignof of the selected public struct, or 0 for an unknown id. + * For every struct that carries a struct_size field, the size returned here + * equals the value its transcribe_*_init() stamps into struct_size. */ +TRANSCRIBE_API size_t transcribe_abi_struct_size(transcribe_abi_struct which); +TRANSCRIBE_API size_t transcribe_abi_struct_align(transcribe_abi_struct which); + /* ----------------------------------------------------------------------- */ /* Logging */ /* ----------------------------------------------------------------------- */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f11c2f58..f1479baa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -125,11 +125,18 @@ add_library(transcribe # (rows summed in the same order). The primary out_w path is the ggml graph # in joint_step; this row-parallelism covers the host fallback and the # remaining host-side matmuls. -find_package(OpenMP QUIET) -if(OpenMP_CXX_FOUND) - set_source_files_properties(arch/parakeet/decoder.cpp PROPERTIES - COMPILE_OPTIONS "${OpenMP_CXX_FLAGS}") - target_link_libraries(transcribe PRIVATE OpenMP::OpenMP_CXX) +# +# Gated on TRANSCRIBE_USE_OPENMP so official provider wheels (which pass it OFF, +# alongside GGML_OPENMP=OFF) do not link an OpenMP runtime into this TU. When +# off, the `#pragma omp` is an ignored no-op and linear() runs serially — the +# same path taken when OpenMP simply is not found. +if(TRANSCRIBE_USE_OPENMP) + find_package(OpenMP QUIET) + if(OpenMP_CXX_FOUND) + set_source_files_properties(arch/parakeet/decoder.cpp PROPERTIES + COMPILE_OPTIONS "${OpenMP_CXX_FLAGS}") + target_link_libraries(transcribe PRIVATE OpenMP::OpenMP_CXX) + endif() endif() target_include_directories(transcribe @@ -145,6 +152,10 @@ target_include_directories(transcribe target_compile_definitions(transcribe PRIVATE TRANSCRIBE_BUILD + # Git commit stamped at configure time; surfaced by + # transcribe_version_commit(). Falls back to "unknown" in the source + # (see transcribe.cpp) when the build tree has no git metadata. + TRANSCRIBE_COMMIT="${TRANSCRIBE_BUILD_COMMIT}" ) # ggml is the runtime substrate. Linked PRIVATE because the public header @@ -158,10 +169,15 @@ target_link_libraries(transcribe # link Accelerate directly; elsewhere we probe for OpenBLAS / MKL / # BLIS via find_package(BLAS). When no BLAS is found the decoder falls # back to a scalar loop (~14x slower but functionally correct). +# Apple Accelerate is a system framework (always present, nothing to vendor), +# so it stays on unconditionally even for wheels. The non-Apple system-BLAS +# probe is gated on TRANSCRIBE_USE_SYSTEM_BLAS: official provider wheels pass it +# OFF so wheel-repair tools never vendor OpenBLAS/MKL/BLIS where it would fight +# another BLAS/OpenMP runtime in the host process. if(APPLE) target_link_libraries(transcribe PRIVATE "-framework Accelerate") target_compile_definitions(transcribe PRIVATE TRANSCRIBE_HAS_BLAS=1) -else() +elseif(TRANSCRIBE_USE_SYSTEM_BLAS) # BLA_VENDOR can be set by the user to prefer a specific BLAS # (e.g. -DBLA_VENDOR=OpenBLAS). find_package probes the system. find_package(BLAS QUIET) @@ -182,11 +198,19 @@ else() else() message(STATUS "transcribe: no BLAS found — decoder uses scalar fallback") endif() +else() + message(STATUS "transcribe: TRANSCRIBE_USE_SYSTEM_BLAS=OFF — decoder uses scalar fallback (official-wheel posture; no system BLAS vendored)") endif() set_target_properties(transcribe PROPERTIES OUTPUT_NAME transcribe POSITION_INDEPENDENT_CODE ON + # Versions the shared lib (libtranscribe.0.1.0.dylib + libtranscribe.0.dylib + # symlink). Ignored for the static archive. SOVERSION is the major only; + # pre-1.0 the real compatibility backstop is the binding's exact + # version/header-hash check, not the SONAME. + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} ) # zlib for Whisper's temperature-fallback compression_ratio metric diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00cd6df6..5c8aa44c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,6 +60,26 @@ add_test( -DTRANSCRIBE_SOURCE_DIR=${PROJECT_SOURCE_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/check_extension_umbrella.cmake) +# ----------------------------------------------------------------------------- +# Shared-library build: only the public-ABI smoke is meaningful +# ----------------------------------------------------------------------------- +# +# Every test below this point is a C++ white-box test: it links `transcribe` +# AND reaches into src/ internal headers, calling non-TRANSCRIBE_API symbols. +# Those symbols are hidden in a shared libtranscribe (visibility=hidden), so the +# white-box suite cannot link against it. A shared build (TRANSCRIBE_BUILD_SHARED +# =ON) is the packaging/binding configuration; build static (the default) to run +# the full suite. The pure-C api_smoke above stays — against a shared lib it +# doubles as an exported-surface canary: it proves every public symbol resolves +# from a clean C consumer with no internal-header reach. +if(BUILD_SHARED_LIBS) + message(STATUS + "transcribe: shared-library build — registering only the public-ABI " + "smoke; the C++ white-box test suite requires a static build " + "(-DTRANSCRIBE_BUILD_SHARED=OFF).") + return() +endif() + # ----------------------------------------------------------------------------- # Backend classification unit test # ----------------------------------------------------------------------------- diff --git a/tests/api_smoke.c b/tests/api_smoke.c index 7c60189b..a328134e 100644 --- a/tests/api_smoke.c +++ b/tests/api_smoke.c @@ -98,6 +98,55 @@ static void test_status_string(void) { CHECK(neg[0] != '\0'); } +static void test_version(void) { + /* transcribe_version() returns a non-empty static string that equals the + * stringized MAJOR.MINOR.PATCH macros the caller compiled against. This is + * the exact-match contract the Python provider version gate relies on. */ + const char * v = transcribe_version(); + CHECK(v != NULL); + CHECK(v[0] != '\0'); + CHECK(strcmp(v, TRANSCRIBE_VERSION) == 0); + + /* The numeric form stays consistent with the components. */ + CHECK(TRANSCRIBE_VERSION_NUMBER == + TRANSCRIBE_VERSION_MAJOR * 10000 + + TRANSCRIBE_VERSION_MINOR * 100 + + TRANSCRIBE_VERSION_PATCH); + + /* Commit is never NULL: "unknown" in a non-git build, a short SHA + * otherwise. */ + const char * commit = transcribe_version_commit(); + CHECK(commit != NULL); + CHECK(commit[0] != '\0'); +} + +static void test_abi_metadata(void) { + /* The native ABI accessors a binding uses to verify its struct layout. + * Known ids report this build's sizeof/alignof; an unknown id reports 0 + * (the documented "cannot verify" sentinel, never a real size). */ + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_RUN_PARAMS) == + sizeof(struct transcribe_run_params)); + CHECK(transcribe_abi_struct_align(TRANSCRIBE_ABI_RUN_PARAMS) == + _Alignof(struct transcribe_run_params)); + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_CAPABILITIES) == + sizeof(struct transcribe_capabilities)); + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_SEGMENT) == + sizeof(struct transcribe_segment)); + CHECK(transcribe_abi_struct_size((transcribe_abi_struct) 9999) == 0); + CHECK(transcribe_abi_struct_align((transcribe_abi_struct) 9999) == 0); + + /* The reported size must equal the struct_size the init function stamps: + * that is the exact value a binding compares its own sizeof against, so the + * two surfaces (ABI accessor and init stamping) must agree. */ + struct transcribe_run_params rp; + transcribe_run_params_init(&rp); + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_RUN_PARAMS) == rp.struct_size); + + struct transcribe_capabilities caps; + transcribe_capabilities_init(&caps); + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_CAPABILITIES) == caps.struct_size); +} + static void test_log_level_values(void) { /* These numeric values must mirror GGML_LOG_LEVEL_* exactly. If this * test ever fails, the public contract documented in transcribe.h @@ -706,6 +755,8 @@ static void test_session_limits_abi(void) { int main(void) { test_status_string(); + test_version(); + test_abi_metadata(); test_log_level_values(); test_log_set_null(); test_init_macros(); From 32fbc9fe5fc38a9d33656899247cbba5c1bd602b Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Jun 2026 16:35:55 +0800 Subject: [PATCH 04/34] continue to get bindings, testing, asan --- .github/workflows/python-bindings.yml | 94 +++++++ CMakeLists.txt | 21 ++ CMakePresets.json | 19 +- .../python/src/transcribe_cpp/__init__.py | 85 +++++- .../python/src/transcribe_cpp/_generated.py | 18 +- .../python/src/transcribe_cpp/_library.py | 18 +- bindings/python/tests/conftest.py | 16 ++ bindings/python/tests/test_backends.py | 80 ++++++ bindings/python/tests/test_lifetime.py | 74 ++++++ bindings/python/tests/test_pcm.py | 111 ++++++++ bindings/python/tests/test_streaming.py | 49 ++++ include/transcribe.h | 106 ++++++++ src/arch/parakeet/decoder.cpp | 23 +- src/transcribe-load-common.cpp | 29 +- src/transcribe-model.h | 11 +- src/transcribe-session.h | 13 + src/transcribe.cpp | 250 +++++++++++++++++- tests/api_smoke.c | 54 ++++ tests/stream_dispatch_unit.cpp | 99 +++++++ 19 files changed, 1143 insertions(+), 27 deletions(-) create mode 100644 bindings/python/tests/test_backends.py create mode 100644 bindings/python/tests/test_lifetime.py create mode 100644 bindings/python/tests/test_pcm.py diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 40b8d6ad..adc33912 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -88,6 +88,100 @@ jobs: - name: Test (full white-box suite) run: ctest --test-dir build --output-on-failure + cpp-tests-sanitized: + # ASan+UBSan over the white-box suite. This is the lane that certifies + # the C lifetime/ABI contracts the FFI bindings rely on — including the + # params copy-out regression (stream_dispatch_unit), which reproduces a + # ctypes-shaped caller freeing its params right after stream_begin. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv (fixtures are generated via uv) + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install build deps + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build zlib1g-dev + - name: Configure (static, sanitized) + run: | + cmake -B build-asan -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DTRANSCRIBE_SANITIZE=ON + - name: Build + run: cmake --build build-asan -j + - name: Test (white-box suite under ASan+UBSan) + run: ctest --test-dir build-asan --output-on-failure + + provider-dl-vulkan: + # Builds the default Linux provider shape — CPU + Vulkan as dynamic + # backend modules (GGML_BACKEND_DL) — assembles it into a flat + # wheel-like directory with $ORIGIN rpaths, deletes the build tree so + # nothing can resolve outside the directory, and proves the Vulkan + # degradation contract on the real runner: + # 1. loader REMOVED (no libvulkan) -> import + CPU devices work, + # vulkan answers unavailable; the module-load failure is quiet. + # 2. mesa lavapipe installed -> the SAME artifacts discover a + # software Vulkan device. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Install build deps (Vulkan SDK pieces + patchelf) + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build zlib1g-dev \ + libvulkan-dev glslc patchelf + - name: Configure (DL provider, CPU + Vulkan modules, wheel posture) + run: | + cmake -B build-dl -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DTRANSCRIBE_BUILD_SHARED=ON \ + -DTRANSCRIBE_GGML_BACKEND_DL=ON \ + -DTRANSCRIBE_VULKAN=ON \ + -DTRANSCRIBE_USE_OPENMP=OFF \ + -DTRANSCRIBE_USE_SYSTEM_BLAS=OFF + - name: Build + run: cmake --build build-dl -j + - name: Assemble flat provider directory ($ORIGIN rpaths, build tree deleted) + run: | + mkdir -p provider + cp -L build-dl/src/libtranscribe.so provider/ + cp -L build-dl/ggml/src/libggml.so.* build-dl/ggml/src/libggml-base.so.* provider/ 2>/dev/null || \ + cp -L build-dl/ggml/src/libggml.so build-dl/ggml/src/libggml-base.so provider/ + cp build-dl/bin/libggml-*.so provider/ + for f in provider/*.so*; do patchelf --set-rpath '$ORIGIN' "$f"; done + ls -la provider/ + rm -rf build-dl # isolation: nothing may resolve outside provider/ + - name: "Tier 1: no Vulkan loader — CPU works, vulkan cleanly unavailable" + run: | + sudo apt-get remove -y libvulkan-dev libvulkan1 || \ + sudo rm -f /usr/lib/x86_64-linux-gnu/libvulkan.so.1* + export TRANSCRIBE_LIBRARY="$PWD/provider/libtranscribe.so" + cat > /tmp/check_no_vulkan.py <<'EOF' + import transcribe_cpp as t + devs = t.backends() + print("devices (no loader):", [(d.name, d.kind) for d in devs]) + assert any(d.kind == "cpu" for d in devs), devs + assert t.backend_available("cpu") + assert not t.backend_available("vulkan"), devs + print("ok: import + CPU fine without a Vulkan loader; vulkan answers False") + EOF + uv run --project bindings/python python /tmp/check_no_vulkan.py + - name: "Tier 2: lavapipe installed — same artifacts discover Vulkan" + run: | + sudo apt-get install -y libvulkan1 mesa-vulkan-drivers + export TRANSCRIBE_LIBRARY="$PWD/provider/libtranscribe.so" + cat > /tmp/check_vulkan.py <<'EOF' + import transcribe_cpp as t + devs = t.backends() + print("devices (lavapipe):", [(d.name, d.kind) for d in devs]) + assert t.backend_available("vulkan"), devs + assert any(d.kind == "vulkan" for d in devs), devs + print("ok: the same provider directory discovers a Vulkan device") + EOF + uv run --project bindings/python python /tmp/check_vulkan.py + python-shared: runs-on: ubuntu-latest steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 09f85cb2..a1c17cd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,15 @@ option(TRANSCRIBE_BUILD_SHARED "Build libtranscribe + ggml as shared librarie option(TRANSCRIBE_USE_OPENMP "Use OpenMP (ggml + Parakeet host-decoder TU)" ON) option(TRANSCRIBE_USE_SYSTEM_BLAS "Link non-Apple system BLAS for the host decoder" ON) +# Dynamic ggml backends: each backend (CPU, Vulkan, CUDA, ...) becomes a +# separate loadable module living NEXT TO libtranscribe instead of being +# compiled in. This is the provider-directory artifact shape the Python +# wheels ship on Linux/Windows: the Vulkan module is present by default and +# simply fails to load (skipped; CPU keeps working) on machines without a +# Vulkan loader/driver. The host loads the module directory once via +# transcribe_init_backends(). Requires TRANSCRIBE_BUILD_SHARED=ON. +option(TRANSCRIBE_GGML_BACKEND_DL "Build ggml backends as loadable modules" OFF) + # Real-model gated tests. OFF by default because the tests need a # converted Parakeet GGUF on disk (~2.4 GB) and CI can't ship one. # When ON, the test reads TRANSCRIBE_REAL_PARAKEET_GGUF from the @@ -216,6 +225,18 @@ set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) # surface. set(BUILD_SHARED_LIBS ${TRANSCRIBE_BUILD_SHARED} CACHE BOOL "" FORCE) +# Dynamic backend modules (see the option's comment above). ggml requires +# shared libs for GGML_BACKEND_DL, so gate it on TRANSCRIBE_BUILD_SHARED +# here for a clear configure-time error. +if(TRANSCRIBE_GGML_BACKEND_DL) + if(NOT TRANSCRIBE_BUILD_SHARED) + message(FATAL_ERROR + "TRANSCRIBE_GGML_BACKEND_DL requires TRANSCRIBE_BUILD_SHARED=ON " + "(backend modules are shared libraries loaded at runtime)") + endif() + set(GGML_BACKEND_DL ON CACHE BOOL "" FORCE) +endif() + # ggml's CMake declares its own warning flags; let it. add_subdirectory(ggml) diff --git a/CMakePresets.json b/CMakePresets.json index 85b745fe..eb12a981 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -42,12 +42,29 @@ "name": "wheel-macos-metal", "inherits": "wheel-base", "displayName": "macOS arm64 Metal wheel", - "description": "Metal ships by default on Apple Silicon, with the shader library embedded so there are no sidecar .metal files to vendor or locate at runtime.", + "description": "Metal ships by default on Apple Silicon, with the shader library embedded so there are no sidecar .metal files to vendor or locate at runtime. Single-artifact (no dynamic backend modules): one platform, one GPU API.", "cacheVariables": { "TRANSCRIBE_METAL": "ON", "GGML_METAL": "ON", "GGML_METAL_EMBED_LIBRARY": "ON" } + }, + { + "name": "wheel-linux-cpu-vulkan", + "inherits": "wheel-linux-cpu", + "displayName": "Linux x86_64 default wheel: CPU + Vulkan backend modules", + "description": "The default Linux provider. GGML_BACKEND_DL builds every backend as a loadable module next to libtranscribe; the wheel ships the conservative CPU module (SIMD tiers off, inherited from wheel-linux-cpu) plus the Vulkan module. On machines without a Vulkan loader/driver the Vulkan module fails to load quietly and CPU keeps working — verified degradation, see docs/python-bindings-distribution-plan.md. Build image needs the Vulkan SDK (glslc).", + "cacheVariables": { + "TRANSCRIBE_GGML_BACKEND_DL": "ON", + "TRANSCRIBE_VULKAN": "ON", + "GGML_VULKAN": "ON" + } + }, + { + "name": "wheel-windows-cpu-vulkan", + "inherits": "wheel-linux-cpu-vulkan", + "displayName": "Windows x86_64 default wheel: CPU + Vulkan backend modules", + "description": "Same provider shape as wheel-linux-cpu-vulkan on Windows: conservative CPU module + Vulkan module, loaded package-locally (the Python loader calls os.add_dll_directory before CDLL). Build image needs the Vulkan SDK (glslc)." } ] } diff --git a/bindings/python/src/transcribe_cpp/__init__.py b/bindings/python/src/transcribe_cpp/__init__.py index a5876c25..e5f4fac1 100644 --- a/bindings/python/src/transcribe_cpp/__init__.py +++ b/bindings/python/src/transcribe_cpp/__init__.py @@ -23,11 +23,12 @@ import ctypes import os import threading +import weakref from dataclasses import dataclass from typing import Literal, Sequence, Union from . import _abi, _generated -from ._library import _base_version, load_library, selected_provider +from ._library import _base_version, artifact_dir, load_library, selected_provider from .errors import ( AbiError, BackendError, @@ -97,6 +98,9 @@ "native_commit", "library_path", "native_provider", + "BackendDevice", + "backends", + "backend_available", "set_log_callback", ] @@ -120,6 +124,22 @@ "matching version or install a matching native provider." ) +# Load ggml backend modules from the artifact directory (package-local; a +# no-op for static/compiled-in builds). In a dynamic-backend build this is +# what registers CPU/Vulkan/... devices, and a Vulkan module on a machine +# without Vulkan simply fails to load while CPU keeps working. Raises only +# when the process ends up with ZERO compute devices. +_artifact = artifact_dir() +if _artifact is not None: + _status = _lib.transcribe_init_backends(os.fspath(_artifact).encode("utf-8")) + if _status != 0: + raise BackendError( + f"no usable compute backend: transcribe_init_backends({_artifact}) " + f"reported {_lib.transcribe_status_string(_status).decode('utf-8', 'replace')}. " + "In a dynamic-backend build the ggml backend modules must sit next " + "to the native library." + ) + _byref = ctypes.byref # Callback function types — must match the generated argtypes for @@ -216,6 +236,41 @@ def native_provider() -> "Union[str, None]": return selected_provider() +@dataclass(frozen=True) +class BackendDevice: + """One registered compute device (owned copies of the C strings).""" + + name: str + description: str + kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "sycl" | "gpu" | "unknown" + + +def backends() -> "list": + """The compute devices registered with the native runtime — what the + process can actually run on, after backend-module loading and graceful + degradation (e.g. a Vulkan module skipped on a machine without Vulkan).""" + devices = [] + for i in range(_lib.transcribe_backend_device_count()): + dev = _generated.transcribe_backend_device() + _lib.transcribe_backend_device_init(_byref(dev)) + _check(_lib.transcribe_get_backend_device(i, _byref(dev)), + f"reading backend device {i}") + devices.append(BackendDevice( + name=_decode(dev.name), + description=_decode(dev.description), + kind=_decode(dev.kind), + )) + return devices + + +def backend_available(backend: Backend) -> bool: + """Whether ``Model(..., backend=...)`` can be satisfied on this machine — + the probe that turns ``backend="vulkan"`` without Vulkan into a clear + answer before any model load.""" + return bool(_lib.transcribe_backend_available( + _enum(_BACKENDS, backend, "backend"))) + + _log_handler = None _log_trampoline = None @@ -637,6 +692,11 @@ class Model: def __init__(self, path: "Union[str, os.PathLike]", *, backend: Backend = "auto", gpu_device: int = 0): + # Live sessions, tracked weakly: close() must free them before the + # model, because transcribe_model_free is only valid once every + # derived session is gone (use-after-free otherwise). Created before + # the load call so the failure path of __del__ finds it. + self._sessions = weakref.WeakSet() params = _ModelLoadParams() _lib.transcribe_model_load_params_init(_byref(params)) params.backend = _enum(_BACKENDS, backend, "backend") @@ -708,7 +768,12 @@ def session(self, *, n_threads: int = 0, kv_type: KVType = "auto", return Session(self, n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx) def close(self) -> None: + """Free the model. Any session still open on it is closed first — + the C contract forbids freeing a model before its sessions, so this + keeps explicit close()/context-manager exit safe in any order.""" if getattr(self, "_handle", None) is not None: + for session in list(getattr(self, "_sessions", ()) or ()): + session.close() _lib.transcribe_model_free(self._handle) self._handle = None @@ -754,6 +819,10 @@ def __init__(self, model: Model, *, n_threads: int = 0, kv_type: KVType = "auto" self._abort_trampoline = _ABORT_CFUNC(lambda _ud: _event.is_set()) _lib.transcribe_set_abort_callback(self._handle, self._abort_trampoline, None) + # Registered last, once the session is fully constructed: Model.close() + # closes any session still tracked here before freeing the model. + model._sessions.add(self) + @property def _h(self) -> ctypes.c_void_p: if self._handle is None: @@ -863,7 +932,12 @@ def stream(self, *, task: Task = "transcribe", language: "Union[str, None]" = No _lib.transcribe_stream_begin(self._h, _byref(run_params), _byref(sp)), "transcribe_stream_begin", ) - return Stream(self) + # The C contract says everything passed to begin may be freed once it + # returns (strings are copied into session-owned storage). The Stream + # still pins the params structs until reset() as defense in depth — + # it costs nothing and keeps the binding safe even against an older + # or out-of-tree native library that predates that contract. + return Stream(self, _keepalive=(run_params, sp, ext)) def _materialize(self, utt: "Union[int, None]" = None) -> Result: """Copy out one result. utt is None for the single-result accessors, or @@ -965,8 +1039,12 @@ class Stream: ``text()``, and call ``finalize()`` when the audio ends. The session is returned to idle on context-manager exit (or ``reset()``).""" - def __init__(self, session: Session): + def __init__(self, session: Session, *, _keepalive=None): self._session = session # keep the session (and its model) alive + # Pins the ctypes params structs (and any family ext) passed to + # transcribe_stream_begin until reset(). The native library copies + # what it needs at begin; this is belt-and-braces for the FFI layer. + self._keepalive = _keepalive self._active = True @property @@ -1018,6 +1096,7 @@ def reset(self) -> None: if self._active: _lib.transcribe_stream_reset(self._session._h) self._active = False + self._keepalive = None def __enter__(self) -> "Stream": return self diff --git a/bindings/python/src/transcribe_cpp/_generated.py b/bindings/python/src/transcribe_cpp/_generated.py index e1897661..aa5aa8f7 100644 --- a/bindings/python/src/transcribe_cpp/_generated.py +++ b/bindings/python/src/transcribe_cpp/_generated.py @@ -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 = "287ebebdeb529e76" +PUBLIC_HEADER_HASH = "0007af60bfcecf7e" # === enum constants === TRANSCRIBE_OK = 0 @@ -48,6 +48,7 @@ TRANSCRIBE_ABI_STREAM_TEXT = 10 TRANSCRIBE_ABI_SESSION_LIMITS = 11 TRANSCRIBE_ABI_EXT = 12 +TRANSCRIBE_ABI_BACKEND_DEVICE = 13 TRANSCRIBE_LOG_LEVEL_NONE = 0 TRANSCRIBE_LOG_LEVEL_INFO = 1 TRANSCRIBE_LOG_LEVEL_WARN = 2 @@ -107,6 +108,8 @@ # === structs === class transcribe_ext(_c.Structure): pass +class transcribe_backend_device(_c.Structure): + pass class transcribe_model_load_params(_c.Structure): pass class transcribe_session_params(_c.Structure): @@ -145,6 +148,7 @@ class transcribe_whisper_chunk_trace(_c.Structure): pass transcribe_ext._fields_ = [("size", _c.c_uint64), ("kind", _c.c_uint32)] +transcribe_backend_device._fields_ = [("struct_size", _c.c_uint64), ("name", _c.c_char_p), ("description", _c.c_char_p), ("kind", _c.c_char_p)] transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("gpu_device", _c.c_int)] transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)] transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)] @@ -168,6 +172,7 @@ class transcribe_whisper_chunk_trace(_c.Structure): # transcribe_abi_struct id per struct (for the native size/align check). ABI_STRUCT_IDS = { 'transcribe_ext': 12, + 'transcribe_backend_device': 13, 'transcribe_model_load_params': 0, 'transcribe_session_params': 1, 'transcribe_run_params': 2, @@ -185,6 +190,7 @@ class transcribe_whisper_chunk_trace(_c.Structure): # C-compiler layout captured at generation (for offset self-check). STRUCT_LAYOUT = { 'transcribe_ext': {'size': 16, 'align': 8, 'offsets': {'size': 0, 'kind': 8}}, + 'transcribe_backend_device': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'name': 8, 'description': 16, 'kind': 24}}, 'transcribe_model_load_params': {'size': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}}, 'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}}, 'transcribe_run_params': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'language': 24, 'target_language': 32, 'keep_special_tags': 40, 'family': 48, 'spec_k_drafts': 56}}, @@ -212,6 +218,12 @@ def configure(lib): lib.transcribe_abi_struct_align.argtypes = [_c.c_int] lib.transcribe_abi_struct_size.restype = _c.c_size_t lib.transcribe_abi_struct_size.argtypes = [_c.c_int] + lib.transcribe_backend_available.restype = _c.c_bool + lib.transcribe_backend_available.argtypes = [_c.c_int] + lib.transcribe_backend_device_count.restype = _c.c_int + lib.transcribe_backend_device_count.argtypes = [] + lib.transcribe_backend_device_init.restype = None + lib.transcribe_backend_device_init.argtypes = [_c.POINTER(transcribe_backend_device)] lib.transcribe_batch_detected_language.restype = _c.c_char_p lib.transcribe_batch_detected_language.argtypes = [_c.c_void_p, _c.c_int] lib.transcribe_batch_full_text.restype = _c.c_char_p @@ -246,6 +258,8 @@ def configure(lib): lib.transcribe_ext_check.argtypes = [_c.POINTER(transcribe_ext), _c.c_uint32, _c.c_uint64] lib.transcribe_full_text.restype = _c.c_char_p lib.transcribe_full_text.argtypes = [_c.c_void_p] + lib.transcribe_get_backend_device.restype = _c.c_int + lib.transcribe_get_backend_device.argtypes = [_c.c_int, _c.POINTER(transcribe_backend_device)] lib.transcribe_get_model.restype = _c.c_void_p lib.transcribe_get_model.argtypes = [_c.c_void_p] lib.transcribe_get_segment.restype = _c.c_int @@ -260,6 +274,8 @@ def configure(lib): lib.transcribe_get_whisper_chunk_trace.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_whisper_chunk_trace)] lib.transcribe_get_word.restype = _c.c_int lib.transcribe_get_word.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_word)] + lib.transcribe_init_backends.restype = _c.c_int + lib.transcribe_init_backends.argtypes = [_c.c_char_p] lib.transcribe_log_set.restype = None lib.transcribe_log_set.argtypes = [_c.CFUNCTYPE(None, _c.c_int, _c.c_char_p, _c.c_void_p), _c.c_void_p] lib.transcribe_model_accepts_ext_kind.restype = _c.c_bool diff --git a/bindings/python/src/transcribe_cpp/_library.py b/bindings/python/src/transcribe_cpp/_library.py index ec6dc234..d501dcc3 100644 --- a/bindings/python/src/transcribe_cpp/_library.py +++ b/bindings/python/src/transcribe_cpp/_library.py @@ -53,6 +53,11 @@ #: None means the library came from the dev-tree / explicit-path fallback. _selected_provider: Optional[str] = None +#: Directory holding the native library and (in dynamic-backend builds) its +#: ggml backend modules. The import-time bootstrap passes this to +#: transcribe_init_backends so module loading stays package-local. +_artifact_dir: Optional[Path] = None + def selected_provider() -> Optional[str]: """Name of the provider package the loaded library came from, or None for a @@ -60,6 +65,11 @@ def selected_provider() -> Optional[str]: return _selected_provider +def artifact_dir() -> Optional[Path]: + """Directory the native library (and any backend modules) live in.""" + return _artifact_dir + + def _library_filename() -> str: if sys.platform == "darwin": return "libtranscribe.dylib" @@ -285,8 +295,9 @@ def load_library(provider: Optional[str] = None) -> tuple: *provider* (or ``TRANSCRIBE_NATIVE_PROVIDER``) forces a specific installed provider; otherwise the best accelerated one is chosen, falling back to CPU. """ - global _selected_provider + global _selected_provider, _artifact_dir _selected_provider = None + _artifact_dir = None # 1. Explicit path override — bypasses provider discovery entirely. override = os.environ.get("TRANSCRIBE_LIBRARY") @@ -296,6 +307,7 @@ def load_library(provider: Optional[str] = None) -> tuple: raise TranscribeError( message=f"TRANSCRIBE_LIBRARY points at a missing file: {path}" ) + _artifact_dir = path.parent return _cdll(path), path # 2. Installed provider packages. @@ -314,6 +326,9 @@ def load_library(provider: Optional[str] = None) -> tuple: ) cdll = _cdll(path) _selected_provider = chosen.name + _artifact_dir = ( + Path(chosen._artifact_dir) if chosen._artifact_dir else path.parent + ) return cdll, path # 3. Bundled / dev-tree fallback. @@ -321,6 +336,7 @@ def load_library(provider: Optional[str] = None) -> tuple: for path in _fallback_candidate_paths(): tried.append(str(path)) if path.is_file(): + _artifact_dir = path.parent return _cdll(path), path raise TranscribeError( diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py index f2716e9f..be41f9ed 100644 --- a/bindings/python/tests/conftest.py +++ b/bindings/python/tests/conftest.py @@ -35,6 +35,13 @@ STREAMING_MODEL = ( REPO / "models/moonshine-streaming-tiny/moonshine-streaming-tiny-Q8_0.gguf" ) +# A streaming model with a language-prompt dictionary: its C++ implementation +# re-reads run_params.language on every feed, which is what the params +# copy-out regression tests exercise. +PROMPTED_STREAMING_MODEL = ( + REPO + / "models/nemotron-3.5-asr-streaming-0.6b/nemotron-3.5-asr-streaming-0.6b-Q8_0.gguf" +) def load_wav(path: Path) -> "array.array": @@ -86,3 +93,12 @@ def streaming_model_path() -> Path: if not STREAMING_MODEL.is_file(): pytest.skip(f"streaming model not present: {STREAMING_MODEL}") return STREAMING_MODEL + + +@pytest.fixture(scope="session") +def prompted_streaming_model_path() -> Path: + if not PROMPTED_STREAMING_MODEL.is_file(): + pytest.skip( + f"prompted streaming model not present: {PROMPTED_STREAMING_MODEL}" + ) + return PROMPTED_STREAMING_MODEL diff --git a/bindings/python/tests/test_backends.py b/bindings/python/tests/test_backends.py new file mode 100644 index 00000000..cd0dc783 --- /dev/null +++ b/bindings/python/tests/test_backends.py @@ -0,0 +1,80 @@ +"""Backend-module loading and device discovery. + +No model required: importing the package already called +``transcribe_init_backends`` on the artifact directory (a no-op for +compiled-in builds, the module-loading step for dynamic-backend builds). +These pin the discovery surface and the degradation contract on whatever +build configuration the suite runs against. +""" + +from __future__ import annotations + +import pytest + +import transcribe_cpp as t +from transcribe_cpp import _library + + +def test_at_least_one_device_registered(): + devices = t.backends() + assert devices, "no compute devices registered — nothing could run" + for dev in devices: + assert dev.name + assert dev.kind in { + "cpu", "accel", "metal", "vulkan", "cuda", "sycl", "gpu", "unknown" + } + + +def test_cpu_always_available(): + # Every shipped configuration includes a CPU backend (compiled in or as + # the baseline module); a process without one is mispackaged. + assert t.backend_available("cpu") is True + assert t.backend_available("auto") is True + assert any(d.kind == "cpu" for d in t.backends()) + + +def test_unavailable_backend_answers_false_not_error(): + # The degradation probe: asking about every known kind must answer a + # bool, never raise — this is what turns backend="vulkan" on a machine + # without Vulkan into a clear Python-level answer. + for kind in ("cpu", "metal", "vulkan", "cuda", "cpu_accel"): + assert t.backend_available(kind) in (True, False) + + +def test_unknown_backend_kind_raises(): + with pytest.raises(t.InvalidArgument, match="unknown backend"): + t.backend_available("quantum") + + +def test_available_kinds_match_device_list(): + kinds = {d.kind for d in t.backends()} + for request, device_kind in (("metal", "metal"), ("vulkan", "vulkan"), + ("cuda", "cuda")): + assert t.backend_available(request) == (device_kind in kinds) + + +def test_artifact_dir_is_library_dir(): + # The loader records where the native artifact lives; backend modules + # are loaded from there and nowhere else (package-local contract). + adir = _library.artifact_dir() + assert adir is not None + assert adir == _library._artifact_dir + import pathlib + + assert pathlib.Path(t.library_path()).parent == adir + + +def test_init_backends_rejects_bad_dirs(): + lib = t._lib + # NULL/empty/missing directory: clean status codes, never a crash. + assert lib.transcribe_init_backends(None) != 0 + assert lib.transcribe_init_backends(b"") != 0 + assert lib.transcribe_init_backends(b"/nonexistent-transcribe-dir") != 0 + + +def test_init_backends_idempotent(): + lib = t._lib + adir = str(_library.artifact_dir()).encode("utf-8") + n = lib.transcribe_backend_device_count() + assert lib.transcribe_init_backends(adir) == 0 + assert lib.transcribe_backend_device_count() == n # no re-registration diff --git a/bindings/python/tests/test_lifetime.py b/bindings/python/tests/test_lifetime.py new file mode 100644 index 00000000..8e2439de --- /dev/null +++ b/bindings/python/tests/test_lifetime.py @@ -0,0 +1,74 @@ +"""Handle-lifetime tests: close ordering, use-after-close, idempotency. + +The C contract says a model may only be freed after every derived session +(`transcribe_model_free` docs). The binding enforces that ordering itself: +``Model.close()`` closes live sessions first, and every closed handle turns +later calls into TranscribeError instead of native use-after-free. +""" + +from __future__ import annotations + +import gc + +import pytest + +import transcribe_cpp as t + + +def test_model_close_closes_open_sessions(model_path): + model = t.Model(model_path) + session = model.session() + model.close() # must close the session first, then free the model + with pytest.raises(t.TranscribeError, match="closed"): + session.run([0.0] * 1600) + + +def test_model_context_exit_with_live_session(model_path, audio_pcm): + # The session is deliberately NOT closed before the model's context + # exits — historically a native use-after-free footgun. + with t.Model(model_path) as model: + session = model.session() + result = session.run(audio_pcm) + assert result.text.strip() + with pytest.raises(t.TranscribeError, match="closed"): + session.run(audio_pcm) + with pytest.raises(t.TranscribeError, match="closed"): + model.session() + + +def test_close_is_idempotent(model_path): + model = t.Model(model_path) + session = model.session() + session.close() + session.close() + model.close() + model.close() + + +def test_session_close_then_model_still_usable(model_path, audio_pcm): + with t.Model(model_path) as model: + session = model.session() + session.close() + # Closing one session must not affect the model or new sessions. + with model.session() as fresh: + assert fresh.run(audio_pcm).text.strip() + + +def test_session_close_with_active_stream(streaming_model_path, audio_pcm): + # Closing the session out from under an active stream must surface as a + # Python error on the next stream call, never a native crash. + with t.Model(streaming_model_path) as model: + session = model.session() + stream = session.stream() + stream.feed(audio_pcm[:16000]) + session.close() + with pytest.raises(t.TranscribeError, match="closed"): + stream.feed(audio_pcm[:16000]) + + +def test_gc_order_session_keeps_model_alive(model_path, audio_pcm): + session = t.Model(model_path).session() # model reference dropped at once + gc.collect() # the session's strong ref must keep the model alive + assert session.run(audio_pcm).text.strip() + session.close() + gc.collect() diff --git a/bindings/python/tests/test_pcm.py b/bindings/python/tests/test_pcm.py new file mode 100644 index 00000000..c7d12704 --- /dev/null +++ b/bindings/python/tests/test_pcm.py @@ -0,0 +1,111 @@ +"""PCM input-validation battery for the FFI boundary. + +The contract: 16 kHz mono float32, as a buffer-protocol object, raw bytes, +or a sequence of floats. Everything else must raise InvalidArgument BEFORE +any native call — a wrong dtype silently reinterpreted as float32 would +produce garbage transcription, not an error, so the gate lives in Python. + +These drive the module-level ``_pcm_to_carray`` helper directly: it is the +single choke point ``run`` / ``run_batch`` / ``Stream.feed`` all share, and +testing it needs no model on disk. +""" + +from __future__ import annotations + +import array +import ctypes + +import pytest + +import transcribe_cpp as t +from transcribe_cpp import InvalidArgument, _pcm_to_carray + + +# --- accepted forms --------------------------------------------------------- + + +def test_float32_array_module(): + arr, n = _pcm_to_carray(array.array("f", [0.0, 0.5, -0.5])) + assert n == 3 + assert abs(arr[1] - 0.5) < 1e-6 + + +def test_raw_bytes_interpreted_as_float32_le(): + raw = array.array("f", [1.0, -1.0]).tobytes() + arr, n = _pcm_to_carray(raw) + assert n == 2 + assert arr[0] == 1.0 and arr[1] == -1.0 + + +def test_bytearray_and_memoryview(): + raw = bytearray(array.array("f", [0.25]).tobytes()) + assert _pcm_to_carray(raw)[1] == 1 + assert _pcm_to_carray(memoryview(raw))[1] == 1 + + +def test_sequence_of_floats(): + arr, n = _pcm_to_carray([0.0, 0.1, 0.2, 0.3]) + assert n == 4 + + +def test_ctypes_float_array_fast_path_no_copy(): + src = (ctypes.c_float * 3)(0.0, 1.0, 2.0) + arr, n = _pcm_to_carray(src) + assert arr is src # documented zero-copy fast path + assert n == 3 + + +# --- rejected forms --------------------------------------------------------- + + +@pytest.mark.parametrize("empty", [[], b"", bytearray(), array.array("f")]) +def test_empty_buffers_rejected(empty): + with pytest.raises(InvalidArgument, match="empty"): + _pcm_to_carray(empty) + + +def test_odd_length_bytes_rejected(): + with pytest.raises(InvalidArgument, match="multiple of 4"): + _pcm_to_carray(b"\x00\x00\x00\x00\x00") + + +def test_float64_buffer_rejected(): + with pytest.raises(InvalidArgument, match="float32"): + _pcm_to_carray(array.array("d", [0.0, 1.0])) + + +def test_int16_buffer_rejected(): + with pytest.raises(InvalidArgument, match="float32"): + _pcm_to_carray(array.array("h", [0, 1, 2])) + + +def test_non_contiguous_buffer_rejected(): + mv = memoryview(array.array("f", [0.0, 1.0, 2.0, 3.0]))[::2] + with pytest.raises(InvalidArgument, match="contiguous"): + _pcm_to_carray(mv) + + +def test_non_iterable_rejected(): + with pytest.raises((InvalidArgument, TypeError)): + _pcm_to_carray(object()) + + +# --- numpy interop (optional dependency; skipped when absent) --------------- + + +def test_numpy_float32_accepted(): + np = pytest.importorskip("numpy") + arr, n = _pcm_to_carray(np.zeros(160, dtype=np.float32)) + assert n == 160 + + +def test_numpy_float64_rejected(): + np = pytest.importorskip("numpy") + with pytest.raises(InvalidArgument, match="float32"): + _pcm_to_carray(np.zeros(160, dtype=np.float64)) + + +def test_numpy_non_contiguous_rejected(): + np = pytest.importorskip("numpy") + with pytest.raises(InvalidArgument, match="contiguous"): + _pcm_to_carray(np.zeros((4, 160), dtype=np.float32)[:, 0]) diff --git a/bindings/python/tests/test_streaming.py b/bindings/python/tests/test_streaming.py index 6b5fe295..04a7e81c 100644 --- a/bindings/python/tests/test_streaming.py +++ b/bindings/python/tests/test_streaming.py @@ -36,6 +36,55 @@ def test_streaming_real(streaming_model_path, audio_pcm): assert "country" in committed.lower(), committed +def test_streaming_with_language_hint(prompted_streaming_model_path, audio_pcm): + """Regression: params copy-out contract for streaming. + + Parakeet's prompted streaming re-resolves run_params.language on EVERY + feed. The caller's params storage (here: ctypes structs local to + Session.stream()) dies when stream() returns, so the library must have + copied the string into session-owned storage at begin — a retained + caller pointer is a use-after-free on the first feed. The ASan lane is + the real detector; the gc + junk scribbling below makes stale reads + likelier to also misbehave in a normal build.""" + import gc + + with t.Model(prompted_streaming_model_path) as model: + caps = model.capabilities + assert caps.supports_streaming + assert caps.languages, "prompted model should advertise languages" + lang = "en" if "en" in caps.languages else caps.languages[0] + with model.session() as session: + stream = session.stream(language=lang) + # The params structs built inside stream() are now dead. Collect + # and scribble over the freed allocations before the first feed. + gc.collect() + junk = [b"\x5a" * 256 for _ in range(4096)] + with stream: + for i in range(0, len(audio_pcm), 16000): + stream.feed(audio_pcm[i : i + 16000]) + update = stream.finalize() + committed = stream.text().committed + del junk + assert update.is_final + assert "country" in committed.lower(), committed + + +def test_streaming_language_hint_second_family(streaming_model_path, audio_pcm): + """Same contract on moonshine-streaming: it retains a shallow copy of + run_params for its decode path. No live pointer read today, but the + retained copy must stay safe to carry — this pins it under ASan.""" + with t.Model(streaming_model_path) as model: + caps = model.capabilities + if not caps.languages or "en" not in caps.languages: + pytest.skip("model does not advertise an 'en' language hint") + with model.session() as session, session.stream(language="en") as stream: + for i in range(0, len(audio_pcm), 16000): + stream.feed(audio_pcm[i : i + 16000]) + stream.finalize() + committed = stream.text().committed + assert "country" in committed.lower(), committed + + def test_cancellation_aborts_in_flight_feed(streaming_model_path, audio_pcm): with t.Model(streaming_model_path) as model, model.session() as session: with session.stream() as stream: diff --git a/include/transcribe.h b/include/transcribe.h index 8bca3c89..41138e57 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -365,6 +365,7 @@ typedef enum { TRANSCRIBE_ABI_STREAM_TEXT = 10, TRANSCRIBE_ABI_SESSION_LIMITS = 11, TRANSCRIBE_ABI_EXT = 12, + TRANSCRIBE_ABI_BACKEND_DEVICE = 13, } transcribe_abi_struct; /* sizeof / alignof of the selected public struct, or 0 for an unknown id. @@ -661,6 +662,95 @@ typedef enum { TRANSCRIBE_BACKEND_CUDA = 5, } transcribe_backend_request; +/* ----------------------------------------------------------------------- */ +/* Backend modules and device discovery */ +/* ----------------------------------------------------------------------- */ +/* + * In dynamic-backend builds (GGML_BACKEND_DL, selected by the + * TRANSCRIBE_GGML_BACKEND_DL build option), compute backends (CPU, Vulkan, + * CUDA, ...) are separate loadable modules shipped next to the library in a + * provider artifact directory, not compiled into it. A host (a Python + * wheel, a Rust crate, an app bundle) loads them ONCE, before the first + * model load, by pointing the library at that directory. In static builds + * the compiled-in backends are already registered, and this call is a + * harmless no-op for a directory containing no modules. + * + * The search is strictly package-local: only artifact_dir is scanned — + * never the executable directory, the working directory, or any system + * path. (ggml additionally honors the GGML_BACKEND_PATH environment + * variable as an explicit user override naming one out-of-tree backend + * module; an unset environment means a fully package-local load.) + * + * A module whose system dependencies are missing — e.g. the Vulkan module + * on a machine with no Vulkan loader, or with a loader but no driver — + * fails to load quietly and is skipped; the remaining backends keep + * working. That degradation is the designed behavior: ship Vulkan by + * default, fall back to CPU on machines that cannot run it. Use the device + * accessors below to see what actually registered, and + * transcribe_backend_available() to probe one kind. + * + * Idempotent per directory (repeat calls with an already-loaded directory + * return TRANSCRIBE_OK without re-loading) and thread-safe. + * + * Returns: + * TRANSCRIBE_ERR_INVALID_ARG artifact_dir is NULL or empty. + * TRANSCRIBE_ERR_FILE_NOT_FOUND artifact_dir is not an existing directory. + * TRANSCRIBE_ERR_BACKEND after loading, the process has zero + * registered compute devices (a dynamic + * build pointed at a directory with no + * usable modules: nothing could run). + * TRANSCRIBE_OK otherwise. + */ +TRANSCRIBE_API transcribe_status transcribe_init_backends( + const char * artifact_dir); + +/* + * Number of compute devices currently registered with the runtime + * (compiled-in backends plus any modules loaded by + * transcribe_init_backends). A device is something a model can be placed + * on: the CPU, an Apple GPU via Metal, a Vulkan GPU, ... + */ +TRANSCRIBE_API int transcribe_backend_device_count(void); + +/* + * One registered compute device. + * + * name / description / kind are borrowed pointers into runtime-owned + * storage: valid for the life of the process, never freed by the caller. + * + * kind is the library's classification, one of: "cpu", "accel" (a + * host-memory accelerator such as BLAS/AMX), "metal", "vulkan", "cuda", + * "sycl", "gpu" (an unrecognized GPU), or "unknown". + */ +struct transcribe_backend_device { + uint64_t struct_size; /* sizeof(*this); set by _init() */ + const char * name; /* ggml device name, e.g. "Metal" */ + const char * description; /* human-readable, e.g. "Apple M4 Max" */ + const char * kind; /* classified kind string; see above */ +}; + +TRANSCRIBE_API void transcribe_backend_device_init( + struct transcribe_backend_device * p); + +/* + * Fill *out (initialized via transcribe_backend_device_init) with device + * `index` in [0, transcribe_backend_device_count()). + */ +TRANSCRIBE_API transcribe_status transcribe_get_backend_device( + int index, + struct transcribe_backend_device * out); + +/* + * Whether a backend request can be satisfied by some registered device: + * AUTO whenever any device exists; CPU and CPU_ACCEL when a CPU device + * exists; METAL / VULKAN / CUDA when a device of that kind exists. Unknown + * or invalid request values answer false (never an error). This is the + * probe a binding uses to turn `backend="vulkan"` on a machine without + * Vulkan into a clear exception instead of a failed model load. + */ +TRANSCRIBE_API bool transcribe_backend_available( + transcribe_backend_request kind); + /* * Initialization of caller-owned params structs. * @@ -783,6 +873,15 @@ TRANSCRIBE_API void transcribe_session_params_init( * * target_language: target language for translation tasks, or NULL. * + * String-pointer lifetime (language / target_language): caller-owned, and + * the library copies what it needs before the API call returns. This holds + * for transcribe_run / transcribe_run_batch (synchronous) AND for + * transcribe_stream_begin: the dispatcher copies these strings into + * session-owned storage at begin, so the caller may free its params — + * including the strings — the moment begin returns, even though the stream + * keeps running. Bindings rely on this: ctypes/FFI-managed string buffers + * die when the begin wrapper returns. + * * keep_special_tags: keep special vocabulary tags (e.g. <|...|>) in the * returned text fields. Default (false) strips them * for clean transcripts; set true to keep the raw @@ -1803,6 +1902,13 @@ TRANSCRIBE_API transcribe_status transcribe_stream_get_text( * the status in transcribe_stream_last_status. The result snapshot * in this case is whatever the hook wrote before failing — * typically empty. + * + * Params lifetime: everything the caller passes is copied out before this + * function returns. run_params strings (language / target_language) are + * copied into session-owned storage, and family extensions follow their + * documented copy-out contract — so the caller may free both params + * structs and every pointer inside them the moment begin returns, while + * the stream stays ACTIVE. Nothing handed to begin needs to outlive it. */ TRANSCRIBE_API transcribe_status transcribe_stream_begin( struct transcribe_session * session, diff --git a/src/arch/parakeet/decoder.cpp b/src/arch/parakeet/decoder.cpp index 0e919fcc..94bc9bdb 100644 --- a/src/arch/parakeet/decoder.cpp +++ b/src/arch/parakeet/decoder.cpp @@ -20,9 +20,13 @@ #include "transcribe-debug.h" #include "transcribe-log.h" +// ggml-backend.h only — NOT ggml-cpu.h: backend-specific entry points such +// as ggml_backend_cpu_init live inside the loadable CPU module under +// GGML_BACKEND_DL, so the library must reach the CPU backend through the +// registry (ggml_backend_init_by_type / get_proc_address), never by direct +// link-time reference. #include "ggml.h" #include "ggml-backend.h" -#include "ggml-cpu.h" #include "ggml-alloc.h" #include @@ -167,7 +171,7 @@ bool build_joint_weight(HostJoint & j, const ggml_tensor * src_out_w, // A CPU backend used ONLY to allocate the resident weight buffer — never // graph_compute'd (each decode call owns its own compute backend), so it // carries no per-step state and is safe to keep on the shared model. - j.w_backend = ggml_backend_cpu_init(); + j.w_backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr); if (j.w_backend == nullptr) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "parakeet decoder: ggml CPU backend init failed"); return fail(); @@ -256,9 +260,20 @@ bool build_joint_graph(JointGraph & g, const HostJoint & j, int n_threads) { return false; }; - g.backend = ggml_backend_cpu_init(); + g.backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr); if (g.backend == nullptr) return fail(); - ggml_backend_cpu_set_n_threads(g.backend, std::max(1, n_threads)); + // n_threads via proc address: the typed setter is a CPU-module symbol + // under GGML_BACKEND_DL. Absent setter (exotic CPU device) keeps the + // backend's default thread count — correct, just less tuned. + if (ggml_backend_dev_t dev = ggml_backend_get_device(g.backend)) { + auto set_n_threads = (ggml_backend_set_n_threads_t) + ggml_backend_reg_get_proc_address( + ggml_backend_dev_backend_reg(dev), + "ggml_backend_set_n_threads"); + if (set_n_threads != nullptr) { + set_n_threads(g.backend, std::max(1, n_threads)); + } + } ggml_init_params ip {}; ip.mem_size = ggml_tensor_overhead() * 8 + ggml_graph_overhead(); diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index 0590942a..8e1914c1 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -107,20 +108,42 @@ ggml_backend_t init_cpu_backend(const char * error_tag) { return cpu_be; } +bool valid_backend_request(int raw) { + switch (raw) { + case TRANSCRIBE_BACKEND_AUTO: + case TRANSCRIBE_BACKEND_CPU: + case TRANSCRIBE_BACKEND_METAL: + case TRANSCRIBE_BACKEND_VULKAN: + case TRANSCRIBE_BACKEND_CPU_ACCEL: + case TRANSCRIBE_BACKEND_CUDA: + return true; + } + return false; +} + } // namespace transcribe_status init_backends(transcribe_backend_request requested, const char * error_tag, BackendPlan & out) { + // Read the request as raw bytes before any enum-typed load: a C caller + // can pass any int here, and loading an out-of-range value through the + // enum lvalue is UB in C++ (UBSan traps). The raw value is validated by + // the switch; only valid values are stored back as the enum. + int requested_raw = TRANSCRIBE_BACKEND_AUTO; + std::memcpy(&requested_raw, &requested, sizeof(requested_raw)); + out = BackendPlan{}; - out.requested = requested; + out.requested = static_cast( + valid_backend_request(requested_raw) ? requested_raw + : TRANSCRIBE_BACKEND_AUTO); // Explicit switch over the enum so an unknown / garbage value // from a C caller never silently collapses into AUTO. Unknown // values are a programming error on the caller's side, not a // fallback we want to tolerate. - switch (requested) { + switch (requested_raw) { case TRANSCRIBE_BACKEND_CPU: case TRANSCRIBE_BACKEND_CPU_ACCEL: { // CPU primary. The CPU_ACCEL variant additionally layers the @@ -229,7 +252,7 @@ transcribe_status init_backends(transcribe_backend_request requested, // to AUTO — that hides bugs. log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "%s: invalid transcribe_backend_request value %d", - error_tag, static_cast(requested)); + error_tag, requested_raw); return TRANSCRIBE_ERR_INVALID_ARG; } diff --git a/src/transcribe-model.h b/src/transcribe-model.h index 27960bd0..25af4b87 100644 --- a/src/transcribe-model.h +++ b/src/transcribe-model.h @@ -157,9 +157,14 @@ namespace transcribe { // Internal feature-bit helpers. Per-family load() / capability KV // reader calls set_feature; the central probe (transcribe_model_supports) -// reads via has_feature. Both treat out-of-range enums as no-ops / false +// reads via has_feature. Both treat out-of-range values as no-ops / false // so the bitset stays bounded and unused bits remain zero. -inline void set_feature(transcribe_model * m, transcribe_feature f, bool on) { +// +// The feature parameter is int, NOT transcribe_feature, on purpose: the +// public entry point receives whatever int a C caller passed, and loading +// an out-of-range value through the enum type is UB in C++. Enum constants +// convert implicitly, so internal callers are unaffected. +inline void set_feature(transcribe_model * m, int f, bool on) { if (m == nullptr) return; const unsigned bit = static_cast(f); if (bit >= 64) return; @@ -171,7 +176,7 @@ inline void set_feature(transcribe_model * m, transcribe_feature f, bool on) { } } -inline bool has_feature(const transcribe_model * m, transcribe_feature f) { +inline bool has_feature(const transcribe_model * m, int f) { if (m == nullptr) return false; const unsigned bit = static_cast(f); if (bit >= 64) return false; diff --git a/src/transcribe-session.h b/src/transcribe-session.h index 0bdd6716..233546e4 100644 --- a/src/transcribe-session.h +++ b/src/transcribe-session.h @@ -278,6 +278,19 @@ struct transcribe_session { TRANSCRIBE_STREAM_COMMIT_AUTO; uint32_t stream_stable_prefix_agreement_n = 0; + // Session-owned copies of the caller's run-params strings, refreshed on + // every transcribe_stream_begin. The dispatcher hands the family hooks a + // params view whose language/target_language point HERE, so a family + // that captures `*run_params` (parakeet re-reads .language on every + // feed; moonshine_streaming carries the copy through its decode path) + // holds pointers into library-owned storage. The public contract lets + // the caller free every params pointer the moment begin returns — + // retaining a caller pointer is a use-after-free. Stable for the + // stream's lifetime: only the next begin mutates these, and it also + // refreshes every retained copy. + std::string stream_language_owned; + std::string stream_target_language_owned; + // UI-facing streaming text state. `full_text` above remains the raw // model hypothesis. `stream_committed_text` is the append-only public // display/input prefix; `stream_tentative_text` is the current raw diff --git a/src/transcribe.cpp b/src/transcribe.cpp index 42eb2c95..186efa28 100644 --- a/src/transcribe.cpp +++ b/src/transcribe.cpp @@ -25,9 +25,12 @@ #include "transcribe-abi.h" #include "transcribe-arch.h" +#include "transcribe-backend.h" #include "transcribe-log.h" #include "transcribe-session.h" #include "transcribe-loader.h" + +#include "ggml-backend.h" #include "transcribe-model.h" #include "transcribe-tokenizer.h" @@ -45,8 +48,11 @@ #include #include #include +#include #include #include +#include +#include #include #include @@ -104,6 +110,26 @@ extern "C" const char * transcribe_version_commit(void) { return TRANSCRIBE_COMMIT; } +// --------------------------------------------------------------------------- +// Raw enum reads at the public ABI boundary +// --------------------------------------------------------------------------- +// +// C callers can store ANY int in an enum-typed ABI field or pass any int as +// an enum-typed argument — in C that is well-defined. In C++ (this TU), +// loading an out-of-range value through an enum-typed lvalue is undefined +// behavior, and UBSan traps it. So every public entry point reads +// caller-supplied enum values as raw bytes first, validates the raw int, and +// only then (if ever) converts to the enum type. All public enums are +// int-sized; the static_assert below pins one representative. + +static inline int enum_field_raw(const void * field) { + int raw; + std::memcpy(&raw, field, sizeof(raw)); + return raw; +} +static_assert(sizeof(transcribe_backend_request) == sizeof(int), + "public enums must be int-sized for raw boundary reads"); + // --------------------------------------------------------------------------- // ABI metadata // --------------------------------------------------------------------------- @@ -113,7 +139,7 @@ extern "C" const char * transcribe_version_commit(void) { // . Keep this switch in sync with the transcribe_abi_struct enum. extern "C" size_t transcribe_abi_struct_size(transcribe_abi_struct which) { - switch (which) { + switch (enum_field_raw(&which)) { case TRANSCRIBE_ABI_MODEL_LOAD_PARAMS: return sizeof(struct transcribe_model_load_params); case TRANSCRIBE_ABI_SESSION_PARAMS: return sizeof(struct transcribe_session_params); case TRANSCRIBE_ABI_RUN_PARAMS: return sizeof(struct transcribe_run_params); @@ -127,12 +153,13 @@ extern "C" size_t transcribe_abi_struct_size(transcribe_abi_struct which) { case TRANSCRIBE_ABI_STREAM_TEXT: return sizeof(struct transcribe_stream_text); case TRANSCRIBE_ABI_SESSION_LIMITS: return sizeof(struct transcribe_session_limits); case TRANSCRIBE_ABI_EXT: return sizeof(struct transcribe_ext); + case TRANSCRIBE_ABI_BACKEND_DEVICE: return sizeof(struct transcribe_backend_device); } return 0; // unknown id: "cannot verify", never a real size } extern "C" size_t transcribe_abi_struct_align(transcribe_abi_struct which) { - switch (which) { + switch (enum_field_raw(&which)) { case TRANSCRIBE_ABI_MODEL_LOAD_PARAMS: return alignof(struct transcribe_model_load_params); case TRANSCRIBE_ABI_SESSION_PARAMS: return alignof(struct transcribe_session_params); case TRANSCRIBE_ABI_RUN_PARAMS: return alignof(struct transcribe_run_params); @@ -146,6 +173,7 @@ extern "C" size_t transcribe_abi_struct_align(transcribe_abi_struct which) { case TRANSCRIBE_ABI_STREAM_TEXT: return alignof(struct transcribe_stream_text); case TRANSCRIBE_ABI_SESSION_LIMITS: return alignof(struct transcribe_session_limits); case TRANSCRIBE_ABI_EXT: return alignof(struct transcribe_ext); + case TRANSCRIBE_ABI_BACKEND_DEVICE: return alignof(struct transcribe_backend_device); } return 0; } @@ -185,14 +213,17 @@ transcribe_status validate_run_params_common( const transcribe_session * session, const transcribe_run_params * params) { - switch (params->task) { + // Raw-validate every enum field before its first enum-typed load (see + // enum_field_raw). Once a field passes here, downstream typed reads — + // including the per-family handlers' — are defined. + switch (enum_field_raw(¶ms->task)) { case TRANSCRIBE_TASK_TRANSCRIBE: case TRANSCRIBE_TASK_TRANSLATE: break; default: return TRANSCRIBE_ERR_INVALID_ARG; } - switch (params->timestamps) { + switch (enum_field_raw(¶ms->timestamps)) { case TRANSCRIBE_TIMESTAMPS_NONE: case TRANSCRIBE_TIMESTAMPS_AUTO: case TRANSCRIBE_TIMESTAMPS_SEGMENT: @@ -202,6 +233,22 @@ transcribe_status validate_run_params_common( default: return TRANSCRIBE_ERR_INVALID_ARG; } + switch (enum_field_raw(¶ms->pnc)) { + case TRANSCRIBE_PNC_MODE_DEFAULT: + case TRANSCRIBE_PNC_MODE_OFF: + case TRANSCRIBE_PNC_MODE_ON: + break; + default: + return TRANSCRIBE_ERR_INVALID_ARG; + } + switch (enum_field_raw(¶ms->itn)) { + case TRANSCRIBE_ITN_MODE_DEFAULT: + case TRANSCRIBE_ITN_MODE_OFF: + case TRANSCRIBE_ITN_MODE_ON: + break; + default: + return TRANSCRIBE_ERR_INVALID_ARG; + } if (params->timestamps != TRANSCRIBE_TIMESTAMPS_AUTO) { const int req_rank = timestamp_rank(params->timestamps); const int max_rank = timestamp_rank(session->model->caps.max_timestamp_kind); @@ -410,6 +457,12 @@ extern "C" void transcribe_segment_init(struct transcribe_segment * p) { p->struct_size = sizeof(*p); } +extern "C" void transcribe_backend_device_init(struct transcribe_backend_device * p) { + if (p == nullptr) { return; } + std::memset(p, 0, sizeof(*p)); + p->struct_size = sizeof(*p); +} + extern "C" void transcribe_word_init(struct transcribe_word * p) { if (p == nullptr) { return; } std::memset(p, 0, sizeof(*p)); @@ -476,7 +529,10 @@ extern "C" bool transcribe_model_supports( const struct transcribe_model * model, transcribe_feature feature) { - return transcribe::has_feature(model, feature); + // Raw read before any enum-typed load (see enum_field_raw): an unknown + // feature id from a C caller answers false, per the documented probe + // contract, instead of being UB. + return transcribe::has_feature(model, enum_field_raw(&feature)); } namespace { @@ -521,6 +577,8 @@ constexpr size_t k_min_token_size = TRANSCRIBE_FIELD_END(transcribe_token, text); constexpr size_t k_min_timings_size = TRANSCRIBE_FIELD_END(transcribe_timings, decode_ms); +constexpr size_t k_min_backend_device_size = + TRANSCRIBE_FIELD_END(transcribe_backend_device, kind); // k_min_whisper_chunk_trace_size lives in arch/whisper/public.cpp with // the chunk-trace accessor that uses it. @@ -538,7 +596,11 @@ static bool has_field(uint64_t struct_size, size_t field_end) { return struct_size >= static_cast(field_end); } -static bool valid_stream_commit_policy(transcribe_stream_commit_policy policy) { +// Takes the RAW integer, not the enum: a C caller can store any int in the +// struct's enum-typed field, and in C++ loading an out-of-range value through +// the enum lvalue is UB (UBSan trips on it). Callers memcpy the bytes into an +// int and validate before the first enum-typed load. +static bool valid_stream_commit_policy(int policy) { switch (policy) { case TRANSCRIBE_STREAM_COMMIT_AUTO: case TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE: @@ -548,6 +610,112 @@ static bool valid_stream_commit_policy(transcribe_stream_commit_policy policy) { return false; } +// --------------------------------------------------------------------------- +// Backend modules and device discovery +// --------------------------------------------------------------------------- +// +// transcribe_init_backends loads ggml backend modules from ONE caller-named +// directory (a Python wheel's package dir, an app bundle, ...). It never +// widens the search: ggml_backend_load_all_from_path with a non-null path +// scans only that path. ggml tolerates every per-module failure (missing +// system deps such as the Vulkan loader -> dlopen fails -> module skipped), +// which is exactly the ship-Vulkan-by-default degradation contract. + +extern "C" transcribe_status transcribe_init_backends(const char * artifact_dir) { + if (artifact_dir == nullptr || artifact_dir[0] == '\0') { + return TRANSCRIBE_ERR_INVALID_ARG; + } + { + struct stat st {}; + if (::stat(artifact_dir, &st) != 0 || !S_ISDIR(st.st_mode)) { + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "transcribe_init_backends: %s is not an existing directory", + artifact_dir); + return TRANSCRIBE_ERR_FILE_NOT_FOUND; + } + } + + // Idempotency: loading the same directory twice would re-dlopen and + // re-register every module (duplicate devices). Keyed on the canonical + // path so two spellings of one directory count as one load. + static std::mutex s_mutex; + static std::set s_loaded_dirs; + std::lock_guard lock(s_mutex); + + std::error_code ec; + std::filesystem::path canonical = + std::filesystem::weakly_canonical(artifact_dir, ec); + const std::string key = ec ? std::string(artifact_dir) + : canonical.string(); + if (s_loaded_dirs.find(key) == s_loaded_dirs.end()) { + ggml_backend_load_all_from_path(artifact_dir); + s_loaded_dirs.insert(key); + } + + if (ggml_backend_dev_count() == 0) { + // Dynamic-backend build pointed at a directory with no usable + // modules: the process has nothing to compute on. Loud and early + // beats a confusing model-load failure later. + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "transcribe_init_backends: no compute devices registered after " + "loading %s (no usable ggml backend modules found there, and no " + "backends are compiled in)", artifact_dir); + return TRANSCRIBE_ERR_BACKEND; + } + return TRANSCRIBE_OK; +} + +extern "C" int transcribe_backend_device_count(void) { + return static_cast(ggml_backend_dev_count()); +} + +extern "C" transcribe_status transcribe_get_backend_device( + int index, + struct transcribe_backend_device * out) +{ + if (out == nullptr) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + if (const auto st = check_struct_size(out->struct_size, k_min_backend_device_size); + st != TRANSCRIBE_OK) + { + return st; + } + if (index < 0 || index >= static_cast(ggml_backend_dev_count())) { + return TRANSCRIBE_ERR_INVALID_ARG; + } + ggml_backend_dev_t dev = ggml_backend_dev_get(static_cast(index)); + out->name = ggml_backend_dev_name(dev); + out->description = ggml_backend_dev_description(dev); + out->kind = transcribe::kind_name(transcribe::classify_device(dev)); + return TRANSCRIBE_OK; +} + +extern "C" bool transcribe_backend_available(transcribe_backend_request kind) { + // Raw read before any enum-typed load (see enum_field_raw); unknown + // values answer false per the documented probe contract. + const int raw = enum_field_raw(&kind); + const size_t n = ggml_backend_dev_count(); + if (raw == TRANSCRIBE_BACKEND_AUTO) { + return n > 0; + } + transcribe::BackendKind want; + switch (raw) { + case TRANSCRIBE_BACKEND_CPU: + case TRANSCRIBE_BACKEND_CPU_ACCEL: want = transcribe::BackendKind::Cpu; break; + case TRANSCRIBE_BACKEND_METAL: want = transcribe::BackendKind::Metal; break; + case TRANSCRIBE_BACKEND_VULKAN: want = transcribe::BackendKind::Vulkan; break; + case TRANSCRIBE_BACKEND_CUDA: want = transcribe::BackendKind::Cuda; break; + default: return false; + } + for (size_t i = 0; i < n; ++i) { + if (transcribe::classify_device(ggml_backend_dev_get(i)) == want) { + return true; + } + } + return false; +} + enum class StreamStablePrefixImpl { GenericTextAgreement, FamilyTokenAgreement, @@ -989,6 +1157,25 @@ extern "C" transcribe_status transcribe_model_load_file( return TRANSCRIBE_ERR_INVALID_ARG; } + // Raw-validate the backend request before the families' first + // enum-typed load of it (see enum_field_raw). init_backends re-checks + // as defense in depth; this boundary check is what makes every + // downstream `params->backend` read defined. + switch (enum_field_raw(¶ms->backend)) { + case TRANSCRIBE_BACKEND_AUTO: + case TRANSCRIBE_BACKEND_CPU: + case TRANSCRIBE_BACKEND_METAL: + case TRANSCRIBE_BACKEND_VULKAN: + case TRANSCRIBE_BACKEND_CPU_ACCEL: + case TRANSCRIBE_BACKEND_CUDA: + break; + default: + transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, + "transcribe_model_load_file: invalid backend request %d", + enum_field_raw(¶ms->backend)); + return TRANSCRIBE_ERR_INVALID_ARG; + } + // Stage 0: format sniff. Two formats are supported today: // GGUF magic 0x46554747 ("GGUF") — the canonical path. // ggml magic 0x67676d6c — legacy whisper.cpp `.bin`. @@ -1107,6 +1294,17 @@ extern "C" transcribe_status transcribe_session_init( if (params->n_threads < 0) { return TRANSCRIBE_ERR_INVALID_ARG; } + // Raw-validate kv_type before the families' first enum-typed load of it + // (see enum_field_raw): an unknown value from a C caller is a clean + // error here, never UB downstream. + switch (enum_field_raw(¶ms->kv_type)) { + case TRANSCRIBE_KV_TYPE_AUTO: + case TRANSCRIBE_KV_TYPE_F32: + case TRANSCRIBE_KV_TYPE_F16: + break; + default: + return TRANSCRIBE_ERR_INVALID_ARG; + } // n_ctx is a trailing field (appended after kv_type), so only read it // when the caller's struct_size actually covers it; an older caller's // smaller struct leaves it at the default 0 = "model max". A negative @@ -1279,17 +1477,24 @@ extern "C" transcribe_status transcribe_stream_begin( { return st; } - transcribe_stream_commit_policy commit_policy = - TRANSCRIBE_STREAM_COMMIT_AUTO; + // Read the caller's commit_policy as raw bytes and validate BEFORE the + // first load through the enum-typed lvalue: C callers can legally store + // any int there, and an out-of-range enum load is UB in C++. + int commit_policy_raw = TRANSCRIBE_STREAM_COMMIT_AUTO; uint32_t stable_prefix_agreement_n = 0; if (has_field(stream_params->struct_size, k_stream_params_commit_policy_size)) { - commit_policy = stream_params->commit_policy; + static_assert(sizeof(stream_params->commit_policy) == sizeof(int), + "commit_policy must be int-sized for the raw read"); + std::memcpy(&commit_policy_raw, &stream_params->commit_policy, + sizeof(commit_policy_raw)); } - if (!valid_stream_commit_policy(commit_policy)) { + if (!valid_stream_commit_policy(commit_policy_raw)) { return TRANSCRIBE_ERR_INVALID_ARG; } + const transcribe_stream_commit_policy commit_policy = + static_cast(commit_policy_raw); if (has_field(stream_params->struct_size, k_stream_params_agreement_n_size)) { @@ -1378,8 +1583,31 @@ extern "C" transcribe_status transcribe_stream_begin( session->stream_commit_policy = commit_policy; session->stream_stable_prefix_agreement_n = stable_prefix_agreement_n; + // Hand the family hook a params view whose pointers the LIBRARY owns. + // Families may capture `*run_params` for the stream's lifetime + // (parakeet re-reads .language on every feed), and the public contract + // lets the caller free every params pointer once begin returns — so the + // strings are copied into session storage here and the view repointed + // at it. `family` (the run-slot extension) is nulled in the view: no + // family reads it on the stream path today, and per the ext copy-out + // contract a retained pointer to it would dangle; a future family that + // wants a run-slot ext at stream begin must plumb it deliberately. + session->stream_language_owned = + run_params->language != nullptr ? run_params->language : ""; + session->stream_target_language_owned = + run_params->target_language != nullptr ? run_params->target_language + : ""; + struct transcribe_run_params run_params_owned = *run_params; + run_params_owned.language = + run_params->language != nullptr + ? session->stream_language_owned.c_str() : nullptr; + run_params_owned.target_language = + run_params->target_language != nullptr + ? session->stream_target_language_owned.c_str() : nullptr; + run_params_owned.family = nullptr; + const transcribe_status st = session->model->arch->stream_begin( - session, run_params, stream_params); + session, &run_params_owned, stream_params); if (st != TRANSCRIBE_OK) { // Family hook rejected the begin (config it does not understand, // memory allocation failure, etc.). Roll lifecycle back to diff --git a/tests/api_smoke.c b/tests/api_smoke.c index a328134e..510c4e09 100644 --- a/tests/api_smoke.c +++ b/tests/api_smoke.c @@ -147,6 +147,59 @@ static void test_abi_metadata(void) { CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_CAPABILITIES) == caps.struct_size); } +static void test_backend_devices(void) { + /* A build has at least one compute device once backends are available: + * compiled-in builds register at startup; dynamic-backend builds + * register via transcribe_init_backends. This smoke runs in both + * configurations. */ + const int n_before = transcribe_backend_device_count(); + CHECK(n_before >= 0); + + /* init_backends argument contract. */ + CHECK(transcribe_init_backends(NULL) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(transcribe_init_backends("") == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(transcribe_init_backends("/nonexistent-transcribe-artifact-dir") == + TRANSCRIBE_ERR_FILE_NOT_FOUND); + + /* An existing directory with no modules: OK when backends are already + * registered (static build), ERR_BACKEND when a dynamic build ends up + * with zero devices. Idempotent: a repeat call reports the same and + * never re-registers (device count stable). */ + const int st1 = transcribe_init_backends("."); + CHECK(st1 == TRANSCRIBE_OK || st1 == TRANSCRIBE_ERR_BACKEND); + const int n_after = transcribe_backend_device_count(); + const int st2 = transcribe_init_backends("."); + CHECK(st2 == st1); + CHECK(transcribe_backend_device_count() == n_after); + + if (n_after > 0) { + struct transcribe_backend_device dev; + transcribe_backend_device_init(&dev); + CHECK(dev.struct_size == sizeof(dev)); + CHECK(transcribe_get_backend_device(0, &dev) == TRANSCRIBE_OK); + CHECK(dev.name != NULL && dev.name[0] != '\0'); + CHECK(dev.description != NULL); + CHECK(dev.kind != NULL && dev.kind[0] != '\0'); + + CHECK(transcribe_backend_available(TRANSCRIBE_BACKEND_AUTO)); + CHECK(transcribe_backend_available(TRANSCRIBE_BACKEND_CPU)); + } + + /* Out-of-range probes answer cleanly: error status or false, never UB. */ + struct transcribe_backend_device dev2; + transcribe_backend_device_init(&dev2); + CHECK(transcribe_get_backend_device(-1, &dev2) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(transcribe_get_backend_device(1 << 20, &dev2) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(transcribe_get_backend_device(0, NULL) == TRANSCRIBE_ERR_INVALID_ARG); + CHECK(!transcribe_backend_available((transcribe_backend_request) 999)); + + /* ABI accessors must know the new struct. */ + CHECK(transcribe_abi_struct_size(TRANSCRIBE_ABI_BACKEND_DEVICE) == + sizeof(struct transcribe_backend_device)); + CHECK(transcribe_abi_struct_align(TRANSCRIBE_ABI_BACKEND_DEVICE) == + _Alignof(struct transcribe_backend_device)); +} + static void test_log_level_values(void) { /* These numeric values must mirror GGML_LOG_LEVEL_* exactly. If this * test ever fails, the public contract documented in transcribe.h @@ -757,6 +810,7 @@ int main(void) { test_status_string(); test_version(); test_abi_metadata(); + test_backend_devices(); test_log_level_values(); test_log_set_null(); test_init_macros(); diff --git a/tests/stream_dispatch_unit.cpp b/tests/stream_dispatch_unit.cpp index 9f290108..7d6b89b3 100644 --- a/tests/stream_dispatch_unit.cpp +++ b/tests/stream_dispatch_unit.cpp @@ -1378,6 +1378,104 @@ void test_two_sessions_independent_streams() { g_abort_flag = false; } +// --- params copy-out contract ---------------------------------------------- +// +// Families may capture `*run_params` at stream_begin and re-read its string +// fields on every feed (parakeet resolves the language prompt per chunk). +// The public contract says the caller may free everything it passed the +// moment begin returns, so the dispatcher must hand the family a params view +// whose strings live in session-owned storage. This test is the FFI-shaped +// repro: params + language string on the heap, scribbled and freed right +// after begin, then a feed that reads the family's retained copy. Without +// the dispatcher copy the retained pointer aliases the caller's freed buffer +// (ASan: heap-use-after-free; plain build: the scribbled bytes); with it the +// feed must see the original string. + +transcribe_run_params g_retained_rp; // the family-side shallow copy +std::string g_language_seen_at_feed; + +transcribe_status retain_stream_begin( + transcribe_session * session, + const transcribe_run_params * run_params, + const transcribe_stream_params * stream_params) +{ + (void)session; + (void)stream_params; + g_retained_rp = *run_params; // exactly what parakeet/moonshine do + return TRANSCRIBE_OK; +} + +transcribe_status retain_stream_feed( + transcribe_session * session, + const float * pcm, + int n_samples, + transcribe_stream_update * update) +{ + (void)session; + (void)pcm; + (void)n_samples; + (void)update; + g_language_seen_at_feed = + g_retained_rp.language != nullptr ? g_retained_rp.language : ""; + return TRANSCRIBE_OK; +} + +void test_begin_copies_param_strings_out() { + const transcribe::Arch arch = { + "fake-retaining-stream", + nullptr, + nullptr, + nullptr, + nullptr, // run_batch + nullptr, // stream_validate + retain_stream_begin, + retain_stream_feed, + fake_stream_finalize, + nullptr, + fake_accepts_no_ext, + }; + + transcribe_model model; + model.arch = &arch; + model.caps.supports_streaming = true; + model.caps.max_timestamp_kind = TRANSCRIBE_TIMESTAMPS_NONE; + + transcribe_session session; + session.model = &model; + + // FFI-shaped caller: the params struct and its language string live on + // the heap and are destroyed the moment begin returns (a ctypes binding + // garbage-collects them exactly like this). + auto * rp = static_cast( + std::malloc(sizeof(transcribe_run_params))); + transcribe_run_params_init(rp); + char * lang = static_cast(std::malloc(16)); + std::snprintf(lang, 16, "en-US"); + rp->language = lang; + + transcribe_stream_params sp; transcribe_stream_params_init(&sp); + g_language_seen_at_feed.clear(); + CHECK(transcribe_stream_begin(&session, rp, &sp) == TRANSCRIBE_OK); + + // Caller destroys its storage: scribble (still owned, so well-defined), + // then free. + std::memset(lang, 'X', 15); + std::memset(rp, 0x5A, sizeof(*rp)); + std::free(lang); + std::free(rp); + + transcribe_stream_update up; transcribe_stream_update_init(&up); + const float pcm[160] = {}; + CHECK(transcribe_stream_feed(&session, pcm, 160, &up) == TRANSCRIBE_OK); + CHECK(g_language_seen_at_feed == "en-US"); + // The run-slot family ext pointer must not survive into the retained + // copy either: it is consumed during begin per its copy-out contract, + // and a stale pointer would dangle just like the strings. + CHECK(g_retained_rp.family == nullptr); + + transcribe_stream_reset(&session); +} + } // namespace int main() { @@ -1418,6 +1516,7 @@ int main() { test_feed_failure_transitions_to_failed(); test_finalize_failure_transitions_to_failed(); test_feed_abort_sets_was_aborted(); + test_begin_copies_param_strings_out(); test_two_sessions_independent_streams(); return g_failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } From f9cff80bee149dcdee95d8564babf89c6b0a73f9 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Jun 2026 17:16:47 +0800 Subject: [PATCH 05/34] fix ci --- CMakeLists.txt | 4 ++++ src/transcribe.cpp | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a1c17cd3..7147cd09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,10 @@ if(TRANSCRIBE_GGML_BACKEND_DL) "(backend modules are shared libraries loaded at runtime)") endif() set(GGML_BACKEND_DL ON CACHE BOOL "" FORCE) + # ggml hard-errors on GGML_NATIVE + GGML_BACKEND_DL (x86): native tuning + # is incompatible with feature-scored modules. A module build is a + # distribution build, so never tune it to the build host. + set(GGML_NATIVE OFF CACHE BOOL "" FORCE) endif() # ggml's CMake declares its own warning flags; let it. diff --git a/src/transcribe.cpp b/src/transcribe.cpp index 186efa28..9e27fd44 100644 --- a/src/transcribe.cpp +++ b/src/transcribe.cpp @@ -610,6 +610,8 @@ static bool valid_stream_commit_policy(int policy) { return false; } +} // namespace + // --------------------------------------------------------------------------- // Backend modules and device discovery // --------------------------------------------------------------------------- @@ -620,6 +622,11 @@ static bool valid_stream_commit_policy(int policy) { // scans only that path. ggml tolerates every per-module failure (missing // system deps such as the Vulkan loader -> dlopen fails -> module skipped), // which is exactly the ship-Vulkan-by-default degradation contract. +// +// NOTE: these definitions must sit at FILE scope, outside the anonymous +// namespace above. clang exports extern "C" functions defined inside an +// unnamed namespace; gcc gives them internal linkage, which strips them +// from the shared library on Linux (undefined references at link). extern "C" transcribe_status transcribe_init_backends(const char * artifact_dir) { if (artifact_dir == nullptr || artifact_dir[0] == '\0') { @@ -716,6 +723,8 @@ extern "C" bool transcribe_backend_available(transcribe_backend_request kind) { return false; } +namespace { + enum class StreamStablePrefixImpl { GenericTextAgreement, FamilyTokenAgreement, From 0092271f3d729c10c49019ed8b01544dd0560ce9 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Jun 2026 20:46:52 +0800 Subject: [PATCH 06/34] ci --- .github/workflows/python-bindings.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index adc33912..2d362b19 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -172,6 +172,11 @@ jobs: run: | sudo apt-get install -y libvulkan1 mesa-vulkan-drivers export TRANSCRIBE_LIBRARY="$PWD/provider/libtranscribe.so" + # lavapipe reports VK_PHYSICAL_DEVICE_TYPE_CPU, and ggml-vulkan's + # default selection takes only discrete/integrated GPUs. The env + # override bypasses the type filter so the software device counts — + # CI-only; real GPUs need no override. + export GGML_VK_VISIBLE_DEVICES=0 cat > /tmp/check_vulkan.py <<'EOF' import transcribe_cpp as t devs = t.backends() From 474ced099c4c1f76b0c80d86308f0eda7b81dac0 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Jun 2026 22:03:39 +0800 Subject: [PATCH 07/34] python native package --- .github/workflows/python-bindings.yml | 54 ++- .gitignore | 5 + CMakeLists.txt | 34 ++ bindings/python-native/README.md | 31 ++ bindings/python-native/_contract.py.in | 15 + .../transcribe_cpp_native/__init__.py | 31 ++ .../python/_generate/check_version_sync.py | 13 +- bindings/python/pyproject.toml | 16 + bindings/python/tests/conftest.py | 20 +- .../python/tests/test_provider_discovery.py | 87 +++- bindings/python/uv.lock | 8 +- cmake/python-wheel-install.cmake | 102 ++++ docs/python-bindings-distribution-plan.md | 438 ++++++++++++++++++ pyproject.toml | 99 ++++ 14 files changed, 939 insertions(+), 14 deletions(-) create mode 100644 bindings/python-native/README.md create mode 100644 bindings/python-native/_contract.py.in create mode 100644 bindings/python-native/transcribe_cpp_native/__init__.py create mode 100644 cmake/python-wheel-install.cmake create mode 100644 docs/python-bindings-distribution-plan.md create mode 100644 pyproject.toml diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 2d362b19..d073ed2e 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -120,8 +120,11 @@ jobs: # 1. loader REMOVED (no libvulkan) -> import + CPU devices work, # vulkan answers unavailable; the module-load failure is quiet. # 2. mesa lavapipe installed -> the SAME artifacts discover a - # software Vulkan device. + # software Vulkan device, and (with HF_TOKEN for the canary model) + # actually transcribe on it. runs-on: ubuntu-latest + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} steps: - uses: actions/checkout@v4 - name: Install uv @@ -186,9 +189,36 @@ jobs: print("ok: the same provider directory discovers a Vulkan device") EOF uv run --project bindings/python python /tmp/check_vulkan.py + - name: "Tier 2b: real transcription on the Vulkan device (needs HF_TOKEN)" + if: env.HF_TOKEN != '' + run: | + uv run --no-project --with huggingface_hub \ + hf download handy-computer/whisper-tiny-gguf \ + whisper-tiny-Q5_K_M.gguf --local-dir canary + export TRANSCRIBE_LIBRARY="$PWD/provider/libtranscribe.so" + export GGML_VK_VISIBLE_DEVICES=0 + cat > /tmp/check_vk_transcribe.py <<'EOF' + import array, wave + import transcribe_cpp as t + assert t.backend_available("vulkan") + with wave.open("samples/jfk.wav", "rb") as w: + pcm16 = array.array("h") + pcm16.frombytes(w.readframes(w.getnframes())) + pcm = array.array("f", (s / 32768.0 for s in pcm16)) + with t.Model("canary/whisper-tiny-Q5_K_M.gguf", backend="vulkan") as m: + print("model backend:", m.backend) + with m.session() as s: + text = s.run(pcm, language="en").text + print("text:", text.strip()) + assert "country" in text.lower(), text + print("ok: transcribed end-to-end on the Vulkan (lavapipe) device") + EOF + uv run --project bindings/python python /tmp/check_vk_transcribe.py python-shared: runs-on: ubuntu-latest + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} steps: - uses: actions/checkout@v4 - name: Install uv @@ -209,13 +239,31 @@ jobs: run: cmake --build build-shared -j - name: api_smoke (exported-symbol canary) + extension umbrella run: ctest --test-dir build-shared --output-on-failure - - name: Python no-model tests + # The canary model upgrades this lane from "imports and ABI" to a real + # end-to-end transcription. Guarded on HF_TOKEN (the model repo is + # private for now; forks have no token) — without it the model tests + # skip cleanly, exactly as before. + - name: Download canary models (needs HF_TOKEN) + if: env.HF_TOKEN != '' + run: | + uv run --no-project --with huggingface_hub \ + hf download handy-computer/whisper-tiny-gguf \ + whisper-tiny-Q5_K_M.gguf --local-dir canary + uv run --no-project --with huggingface_hub \ + hf download handy-computer/moonshine-streaming-tiny-gguf \ + moonshine-streaming-tiny-Q8_0.gguf --local-dir canary + echo "TRANSCRIBE_SMOKE_MODEL=$PWD/canary/whisper-tiny-Q5_K_M.gguf" >> "$GITHUB_ENV" + echo "TRANSCRIBE_SMOKE_STREAMING_MODEL=$PWD/canary/moonshine-streaming-tiny-Q8_0.gguf" >> "$GITHUB_ENV" + - name: Python tests (real transcription when the canary is present) run: | # libtranscribe.so is dlopen'd by ctypes; help it resolve its sibling # ggml shared libs in the build tree. export LD_LIBRARY_PATH="$(find "$PWD/build-shared" -name '*.so' \ -printf '%h\n' | sort -u | tr '\n' ':')$LD_LIBRARY_PATH" # The loader auto-discovers build-shared/src/libtranscribe.so from the - # repo root; model tests skip (no GGUF in CI). + # repo root; the TRANSCRIBE_SMOKE_* vars (set above when HF_TOKEN + # exists) un-skip the offline AND streaming model tests. The prompted + # (nemotron) streaming test still skips here — its regression is + # pinned in CI by the sanitized C-level test instead. uv run --project bindings/python --extra test \ pytest bindings/python/tests -q -rs diff --git a/.gitignore b/.gitignore index e8a22031..c4d7a601 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ /build-*/ /cmake-build-*/ +# Python distribution output (provider wheels/sdists at the repo root; +# bindings/python/dist for the pure API package) +/dist/ +/bindings/python/dist/ + # Scratch space for benchmarks, staging, etc. /tmp/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 7147cd09..bd67eca6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,34 @@ set(CMAKE_C_VISIBILITY_PRESET hidden) set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) +# ----------------------------------------------------------------------------- +# Python wheel build (scikit-build-core sets SKBUILD) +# ----------------------------------------------------------------------------- +# +# The repo-root pyproject.toml builds the transcribe-cpp-native provider +# package through scikit-build-core. Force the library shape that package +# ships (shared libtranscribe, no tests/examples/tools) and default to the +# official wheel-hygiene posture: no OpenMP and no non-Apple system BLAS, so +# nothing is vendored that collides with PyTorch/NumPy/MKL in-process, and +# Metal shaders embedded so the artifact has no sidecar files. The hygiene +# defaults (not the shape) can be overridden with -D for local source builds +# that want them. CMP0077 is NEW here, so option() below respects these. +if(SKBUILD) + set(TRANSCRIBE_BUILD_SHARED ON) + set(TRANSCRIBE_BUILD_TESTS OFF) + set(TRANSCRIBE_BUILD_EXAMPLES OFF) + set(TRANSCRIBE_BUILD_TOOLS OFF) + if(NOT DEFINED CACHE{TRANSCRIBE_USE_OPENMP}) + set(TRANSCRIBE_USE_OPENMP OFF) + endif() + if(NOT DEFINED CACHE{TRANSCRIBE_USE_SYSTEM_BLAS}) + set(TRANSCRIBE_USE_SYSTEM_BLAS OFF) + endif() + if(NOT DEFINED CACHE{GGML_METAL_EMBED_LIBRARY}) + set(GGML_METAL_EMBED_LIBRARY ON CACHE BOOL "" FORCE) + endif() +endif() + # ----------------------------------------------------------------------------- # Apple Silicon detection (drives Metal default) # ----------------------------------------------------------------------------- @@ -262,3 +290,9 @@ endif() if(TRANSCRIBE_BUILD_TOOLS) add_subdirectory(tools) endif() + +# Python provider-wheel packaging (after subdirs: needs the target list and +# ggml's GGML_AVAILABLE_BACKENDS). +if(SKBUILD) + include(cmake/python-wheel-install.cmake) +endif() diff --git a/bindings/python-native/README.md b/bindings/python-native/README.md new file mode 100644 index 00000000..2759895c --- /dev/null +++ b/bindings/python-native/README.md @@ -0,0 +1,31 @@ +# transcribe-cpp-native + +Prebuilt native library for [`transcribe-cpp`](https://pypi.org/project/transcribe-cpp/), +the Python bindings for [transcribe.cpp](https://github.com/handy-computer/transcribe.cpp) +(local speech-to-text on ggml). + +You normally don't install this directly — `pip install transcribe-cpp` +depends on it. It carries no Python API; it bundles the `libtranscribe` +shared library plus its ggml backend libraries and registers them with the +bindings via the `transcribe_cpp.native` entry point. + +What each artifact ships: + +| Artifact | Backends | +| --- | --- | +| Linux x86_64 wheel | CPU (conservative baseline) + Vulkan backend module | +| Windows x86_64 wheel | CPU (conservative baseline) + Vulkan backend module | +| macOS arm64 wheel | Metal (embedded shaders) + CPU | +| sdist | builds from source; machine-tuned CPU by default, backends via `CMAKE_ARGS` | + +The Vulkan backend ships as a dynamic ggml module: on machines without a +Vulkan loader or driver it simply fails to load and CPU keeps working — no +crash, no import error. Specialized providers (e.g. CUDA) are separate +packages installed alongside this one via extras of `transcribe-cpp`. + +Building from the sdist requires CMake ≥ 3.21, Ninja, and a C++17 compiler; +backend selection follows the repo's CMake options, e.g.: + +```sh +CMAKE_ARGS="-DTRANSCRIBE_VULKAN=ON" pip install --no-binary :all: transcribe-cpp-native +``` diff --git a/bindings/python-native/_contract.py.in b/bindings/python-native/_contract.py.in new file mode 100644 index 00000000..222a07a8 --- /dev/null +++ b/bindings/python-native/_contract.py.in @@ -0,0 +1,15 @@ +"""Build-time provider contract stamp. Generated by CMake — do not edit. + +See cmake/python-wheel-install.cmake for how each value is derived. +""" + +#: Native library version (TRANSCRIBE_VERSION_* macros in include/transcribe.h). +VERSION = "@PROJECT_VERSION@" + +#: Public-header digest the committed FFI layer (_generated.py) was generated +#: from. The binding refuses to load a provider built against a different ABI. +PUBLIC_HEADER_HASH = "@TRANSCRIBE_PUBLIC_HEADER_HASH@" + +#: Backend kinds compiled into this artifact (modules may still degrade to +#: unavailable at runtime, e.g. Vulkan without a loader/driver). +BACKENDS = (@TRANSCRIBE_WHEEL_BACKENDS_PY@,) diff --git a/bindings/python-native/transcribe_cpp_native/__init__.py b/bindings/python-native/transcribe_cpp_native/__init__.py new file mode 100644 index 00000000..fe47efbf --- /dev/null +++ b/bindings/python-native/transcribe_cpp_native/__init__.py @@ -0,0 +1,31 @@ +"""transcribe-cpp-native: prebuilt native artifact for the transcribe-cpp bindings. + +This package contains no Python API. It bundles the native ``transcribe`` +shared library (plus its ggml libraries and any dynamic backend modules) in +``_native/`` and advertises it to the pure-Python ``transcribe-cpp`` package +through the ``transcribe_cpp.native`` entry point declared in pyproject.toml. + +``descriptor()`` returns the provider contract the binding validates before +dlopen — see bindings/python/src/transcribe_cpp/_library.py for the consuming +side. ``_contract.py`` is generated at build time by CMake +(cmake/python-wheel-install.cmake) and stamps the native library version, the +public-header hash the FFI layer was generated from, and the backend kinds +this artifact was built with. +""" + +from __future__ import annotations + +from pathlib import Path + + +def descriptor() -> dict: + """The transcribe_cpp.native provider contract for this package.""" + from . import _contract + + return { + "name": "transcribe-cpp-native", + "artifact_dir": str(Path(__file__).resolve().parent / "_native"), + "version": _contract.VERSION, + "header_hash": _contract.PUBLIC_HEADER_HASH, + "backends": list(_contract.BACKENDS), + } diff --git a/bindings/python/_generate/check_version_sync.py b/bindings/python/_generate/check_version_sync.py index 0863764a..3df14d5e 100644 --- a/bindings/python/_generate/check_version_sync.py +++ b/bindings/python/_generate/check_version_sync.py @@ -60,10 +60,21 @@ def init_version(text: str) -> str | None: return m.group(1) if m else None +def native_pin_version(text: str) -> str | None: + # The hard dependency on the default provider is the pre-1.0 base-version + # contract at resolver level: transcribe-cpp-native==X.Y.Z.* — X.Y.Z must + # be the same base as everything else. (The provider package itself can't + # drift: its version is parsed from the header at build time.) + m = re.search(r'"transcribe-cpp-native\s*==\s*([0-9.]+?)\.\*"', text) + return m.group(1) if m else None + + def main() -> int: + pyproject_text = PYPROJECT.read_text() sources = { "include/transcribe.h": header_version(HEADER.read_text()), - "pyproject.toml": pyproject_version(PYPROJECT.read_text()), + "pyproject.toml": pyproject_version(pyproject_text), + "pyproject.toml (native pin)": native_pin_version(pyproject_text), "__init__.__version__": init_version(INIT.read_text()), } diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 6c3e6f89..a2e32a0d 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -28,6 +28,14 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", ] +# The pure-Python API package hard-depends on the default native provider so +# `pip install transcribe-cpp` transcribes out of the box. The `==X.Y.Z.*` pin +# is the pre-1.0 base-version contract enforced at the resolver level (a .postN +# packaging fix still resolves); the import-time version/header-hash check in +# _library.py is the runtime backstop. check_version_sync.py gates this pin +# against include/transcribe.h. +dependencies = ["transcribe-cpp-native==0.0.1.*"] + [project.optional-dependencies] # Test-only deps. Run with: uv run --extra test pytest (from bindings/python). test = ["pytest>=7"] @@ -40,6 +48,14 @@ Issues = "https://github.com/handy-computer/transcribe.cpp/issues" [tool.hatch.build.targets.wheel] packages = ["src/transcribe_cpp"] +[tool.uv] +# Dev/CI environments load a locally built library (TRANSCRIBE_LIBRARY or the +# build-tree fallback) — they must not pull the native provider from an index +# (it isn't published until release, and a prebuilt artifact would shadow the +# library under test). The provably-false marker removes the dependency for uv +# project operations only; pip installs of built distributions keep the pin. +override-dependencies = ["transcribe-cpp-native; python_version < '0'"] + [tool.pytest.ini_options] testpaths = ["tests"] # 77 is the historical CTest "skipped" code; pytest uses skip markers instead. diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py index be41f9ed..48dbcf8c 100644 --- a/bindings/python/tests/conftest.py +++ b/bindings/python/tests/conftest.py @@ -90,15 +90,23 @@ def audio_pcm(audio_path: Path) -> "array.array": @pytest.fixture(scope="session") def streaming_model_path() -> Path: - if not STREAMING_MODEL.is_file(): - pytest.skip(f"streaming model not present: {STREAMING_MODEL}") - return STREAMING_MODEL + override = os.environ.get("TRANSCRIBE_SMOKE_STREAMING_MODEL") + path = Path(override) if override else STREAMING_MODEL + if not path.is_file(): + pytest.skip( + f"streaming model not present: {path} " + "(set TRANSCRIBE_SMOKE_STREAMING_MODEL)" + ) + return path @pytest.fixture(scope="session") def prompted_streaming_model_path() -> Path: - if not PROMPTED_STREAMING_MODEL.is_file(): + override = os.environ.get("TRANSCRIBE_SMOKE_PROMPTED_MODEL") + path = Path(override) if override else PROMPTED_STREAMING_MODEL + if not path.is_file(): pytest.skip( - f"prompted streaming model not present: {PROMPTED_STREAMING_MODEL}" + f"prompted streaming model not present: {path} " + "(set TRANSCRIBE_SMOKE_PROMPTED_MODEL)" ) - return PROMPTED_STREAMING_MODEL + return path diff --git a/bindings/python/tests/test_provider_discovery.py b/bindings/python/tests/test_provider_discovery.py index 56d8f7a2..1b02f8d1 100644 --- a/bindings/python/tests/test_provider_discovery.py +++ b/bindings/python/tests/test_provider_discovery.py @@ -163,6 +163,87 @@ def test_contract_accepts_post_release_provider(): p.validate_contract() # no raise -def test_no_provider_installed_in_test_env(): - # Sanity: this binding ran from the dev tree, not a provider package. - assert t.native_provider() is None +def test_provider_state_is_consistent(): + # Two legitimate environments run this suite: the dev tree (no provider + # installed / TRANSCRIBE_LIBRARY override -> native_provider() is None) and + # a wheel-test env where the transcribe-cpp-native package supplied the + # library. Pin that the reported state matches where the library came from. + provider = t.native_provider() + if provider is None: + assert "transcribe_cpp_native" not in t.library_path() + else: + assert provider == "transcribe-cpp-native" + assert "transcribe_cpp_native" in t.library_path() + + +# --- end to end, through the real entry-point machinery -------------------- +# +# The unit tests above drive selection/contract with synthetic _Provider +# objects. These build an actual installed-distribution shape on disk — a +# module plus a .dist-info advertising the transcribe_cpp.native group — and +# run the full path: importlib.metadata discovery → ep.load() → descriptor → +# selection → contract → ctypes dlopen of the real library this suite loaded. + + +def _install_fake_provider( + site_dir, module_name, *, header_hash=GOOD_HASH, version=API_BASE +): + lib = t.library_path() + dist = module_name.replace("_", "-") + (site_dir / f"{module_name}.py").write_text( + "def descriptor():\n" + " return {\n" + f" 'name': {dist!r},\n" + f" 'library_path': {lib!r},\n" + f" 'version': {version!r},\n" + f" 'header_hash': {header_hash!r},\n" + " 'backends': ['cpu'],\n" + " }\n" + ) + di = site_dir / f"{module_name}-0.0.1.dist-info" + di.mkdir() + (di / "METADATA").write_text( + f"Metadata-Version: 2.1\nName: {dist}\nVersion: 0.0.1\n" + ) + (di / "entry_points.txt").write_text( + f"[transcribe_cpp.native]\n{module_name} = {module_name}:descriptor\n" + ) + return dist + + +@pytest.fixture +def library_globals_restored(): + """load_library mutates module globals; put them back for later tests.""" + provider, art = _library._selected_provider, _library._artifact_dir + yield + _library._selected_provider = provider + _library._artifact_dir = art + + +def test_entry_point_end_to_end_loads(tmp_path, monkeypatch, library_globals_restored): + dist = _install_fake_provider(tmp_path, "fake_native_provider_ok") + monkeypatch.syspath_prepend(str(tmp_path)) + monkeypatch.delenv("TRANSCRIBE_LIBRARY", raising=False) + + # Explicit request: deterministic even if a real provider package is also + # installed in this environment (auto-selection policy is unit-tested). + cdll, path = _library.load_library(provider=dist) + assert str(path) == t.library_path() + assert _library.selected_provider() == dist + assert _library.artifact_dir() == path.parent + # The dlopen is real: the loaded handle exposes the C API. + assert cdll.transcribe_version is not None + + +def test_entry_point_end_to_end_rejects_abi_mismatch( + tmp_path, monkeypatch, library_globals_restored +): + dist = _install_fake_provider( + tmp_path, "fake_native_provider_badhash", header_hash="deadbeefdeadbeef" + ) + monkeypatch.syspath_prepend(str(tmp_path)) + monkeypatch.delenv("TRANSCRIBE_LIBRARY", raising=False) + + # The contract check must refuse before any dlopen happens. + with pytest.raises(TranscribeError, match="public ABI"): + _library.load_library(provider=dist) diff --git a/bindings/python/uv.lock b/bindings/python/uv.lock index f3d12110..87d93d5b 100644 --- a/bindings/python/uv.lock +++ b/bindings/python/uv.lock @@ -5,6 +5,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[manifest] +overrides = [{ name = "transcribe-cpp-native", marker = "python_full_version < '0'" }] + [[package]] name = "colorama" version = "0.4.6" @@ -185,7 +188,10 @@ test = [ ] [package.metadata] -requires-dist = [{ name = "pytest", marker = "extra == 'test'", specifier = ">=7" }] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=7" }, + { name = "transcribe-cpp-native", specifier = "==0.0.1.*" }, +] [[package]] name = "typing-extensions" diff --git a/cmake/python-wheel-install.cmake b/cmake/python-wheel-install.cmake new file mode 100644 index 00000000..71965458 --- /dev/null +++ b/cmake/python-wheel-install.cmake @@ -0,0 +1,102 @@ +# Install rules for the transcribe-cpp-native Python provider wheel. +# +# Only included when scikit-build-core drives the build (SKBUILD is set, see +# the gate in the top-level CMakeLists.txt). Everything the wheel ships is +# COMPONENT wheel; pyproject.toml restricts packaging to that component, so +# the vendored ggml's default-component install rules (headers, cmake config, +# pkg-config) never leak into the wheel. +# +# The artifact directory is FLAT: every shared library and ggml backend module +# sits next to libtranscribe in transcribe_cpp_native/_native/, resolved via +# $ORIGIN / @loader_path rpaths (Windows uses os.add_dll_directory in the +# binding's loader). transcribe_init_backends() scans exactly this directory +# for backend modules, so module search never escapes the package. + +# --- the library set -------------------------------------------------------- +# ggml maintains GGML_AVAILABLE_BACKENDS (internal cache) as backends register; +# in GGML_BACKEND_DL builds these are MODULE libraries (the provider-directory +# shape), otherwise ordinary shared libraries linked into libggml. +set(_wheel_targets transcribe ggml ggml-base) +foreach(_backend IN LISTS GGML_AVAILABLE_BACKENDS) + if(TARGET ${_backend}) + list(APPEND _wheel_targets ${_backend}) + endif() +endforeach() +list(REMOVE_DUPLICATES _wheel_targets) + +foreach(_tgt IN LISTS _wheel_targets) + # Strip VERSION/SOVERSION decoration inside the wheel: versioned library + # names install as symlink chains (libggml.so -> libggml.so.0 -> ...), and + # wheels cannot carry symlinks — zip would materialize each link as a full + # duplicate copy. Undecorated names also make the SONAME/install-name the + # plain file name, so sibling resolution needs nothing but the rpath. + # Pre-1.0 the binding's exact version/header-hash check is the real + # compatibility gate, not the SONAME. + set_property(TARGET ${_tgt} PROPERTY VERSION) + set_property(TARGET ${_tgt} PROPERTY SOVERSION) + if(APPLE) + set_property(TARGET ${_tgt} PROPERTY INSTALL_RPATH "@loader_path") + else() + set_property(TARGET ${_tgt} PROPERTY INSTALL_RPATH "$ORIGIN") + endif() + install(TARGETS ${_tgt} + # Shared libraries / backend modules (RUNTIME = DLLs on Windows). + LIBRARY DESTINATION _native COMPONENT wheel + RUNTIME DESTINATION _native COMPONENT wheel + # Windows import .lib stubs: link-time only, never packaged. + ARCHIVE DESTINATION _native-dev COMPONENT wheel-dev) +endforeach() + +# --- the build-time contract stamp (_contract.py) --------------------------- +# The binding hard-fails on a provider whose version or public-header hash +# disagrees with it (_library.validate_contract). Version comes from the +# header macros via PROJECT_VERSION; the header hash is the digest the FFI +# generator emitted into the committed _generated.py (drift-gated in CI), so +# wheel and binding are stamped from the same source. +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/bindings/python/src/transcribe_cpp/_generated.py" + _hash_line REGEX "^PUBLIC_HEADER_HASH = ") +string(REGEX MATCH "\"([0-9a-f]+)\"" _ "${_hash_line}") +set(_public_header_hash "${CMAKE_MATCH_1}") +if(NOT _public_header_hash MATCHES "^[0-9a-f]+$") + message(FATAL_ERROR + "python-wheel-install: failed to parse PUBLIC_HEADER_HASH from " + "bindings/python/src/transcribe_cpp/_generated.py") +endif() + +# Backend kinds this artifact supports, for the descriptor's `backends` field +# (and the binding's accelerated-first provider ranking). Derived from the +# ggml backend target names; CPU ISA variants (ggml-cpu-haswell, ...) all +# collapse to "cpu". +set(_kinds "") +foreach(_backend IN LISTS GGML_AVAILABLE_BACKENDS) + string(REGEX REPLACE "^ggml-" "" _kind "${_backend}") + string(REGEX REPLACE "^cpu-.*$" "cpu" _kind "${_kind}") + list(APPEND _kinds "${_kind}") +endforeach() +list(REMOVE_DUPLICATES _kinds) +# Accelerated kinds first, cpu last — readability only; ranking is the +# binding's job. +set(_backend_kinds "") +foreach(_kind IN ITEMS cuda metal vulkan) + if(_kind IN_LIST _kinds) + list(APPEND _backend_kinds "${_kind}") + list(REMOVE_ITEM _kinds "${_kind}") + endif() +endforeach() +list(REMOVE_ITEM _kinds cpu) +list(APPEND _backend_kinds ${_kinds} cpu) + +list(JOIN _backend_kinds "\", \"" _backends_joined) +set(TRANSCRIBE_WHEEL_BACKENDS_PY "\"${_backends_joined}\"") +set(TRANSCRIBE_PUBLIC_HEADER_HASH "${_public_header_hash}") + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/bindings/python-native/_contract.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/python-wheel/_contract.py" + @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/python-wheel/_contract.py" + DESTINATION . COMPONENT wheel) + +message(STATUS + "python wheel: transcribe-cpp-native ${PROJECT_VERSION} " + "(header ${_public_header_hash}, backends: ${_backend_kinds})") diff --git a/docs/python-bindings-distribution-plan.md b/docs/python-bindings-distribution-plan.md new file mode 100644 index 00000000..b374d72c --- /dev/null +++ b/docs/python-bindings-distribution-plan.md @@ -0,0 +1,438 @@ +# Python Bindings and Distribution Plan + +Direction for first-party Python bindings and native package distribution for +`transcribe.cpp`. The binding layer itself is implemented; this document now +records the settled decisions, the design context for the distribution work +that remains, and the punch list to a public release. + +The short version: + +- Bindings live in-tree at `bindings/python`; the public C API + (`include/transcribe/extensions.h`) is the source of truth. +- Low-level FFI is generated `ctypes` (libclang), committed, drift-gated. +- A small handwritten Pythonic layer owns the user experience. +- Distribution is a pure-Python API package (`transcribe-cpp`) plus + self-contained native provider wheels per platform/backend. +- An sdist that builds from source is the universal fallback. +- Vulkan ships **by default** in the Linux and Windows wheels, as a ggml + dynamic backend module that degrades cleanly to CPU when no Vulkan + loader/driver exists. Metal ships by default on macOS arm64. CUDA is an + opt-in provider built and tested in the same cycle. +- Merge bar: this branch merges release-ready. Everything in the punch list + is merge-blocking unless explicitly marked otherwise. + +## Status: what is already built + +Done and verified (see the `bindings/python/tests/` pytest suite, which +exercises all of it against a locally built shared library): + +- **Generated FFI layer.** `_generate/generate.py` parses the public headers + with libclang and emits `_generated.py`: structs with compiler-computed + layouts, enum/macro constants, function prototypes, and per-struct ABI + metadata. Committed; `--check` is the CI drift gate. +- **ABI verification at import.** `_abi.py` checks every generated struct's + size/alignment/per-field offsets against the captured C-compiler layout, + then size/alignment against the loaded library via the + `transcribe_abi_struct_size/_align` C API. Mismatch raises before any + struct is constructed. +- **High-level API.** `Model`, `Session`, `run`, `run_batch`, streaming + (`Stream`, commit policies, text views), typed family extensions for all + five families, log callback with GIL-safe trampoline, event-based + cancellation, exception hierarchy mapped from `transcribe_status`. Results + are fully materialized (no borrowed pointers escape); ctypes releases the + GIL on every foreign call (verified by test). +- **C/CMake groundwork.** `TRANSCRIBE_VERSION_*` macros as the single version + source (parsed by CMake; surfaced by `transcribe_version()/_commit()`; + stamped as shared-lib VERSION/SOVERSION). Build switches + `TRANSCRIBE_BUILD_SHARED`, `TRANSCRIBE_USE_OPENMP`, + `TRANSCRIBE_USE_SYSTEM_BLAS`. Shared builds register only the public-ABI + smoke (white-box suite requires the static build). +- **PyPI.** `transcribe-cpp` is reserved (0.0.0 placeholder published). + Import package is `transcribe_cpp`. + +## Settled decisions + +Recorded so they are not relitigated; rationale condensed. + +- **Bind the C API, not C++ internals.** Generated `ctypes` rather than + pybind11/nanobind (handwritten glue drifts, and a compiled extension forces + a Python-version × platform × backend wheel matrix) and rather than CFFI + (kept as fallback only). ctypes call overhead is irrelevant at this call + granularity — one FFI call per transcription run, not per token. +- **Pure-Python API package + native provider packages.** PyPI wheel tags + cannot express backend choices, so `transcribe-cpp` stays pure Python and + loads a package-local native artifact supplied by a provider package. The + **default provider on Linux/Windows bundles CPU + Vulkan** as ggml backend + modules (Vulkan degrades cleanly to CPU when no loader/driver exists — + verified, see design context); macOS's default provider is Metal. Bigger + or specialized backends are separate providers installed via extras + (`pip install "transcribe-cpp[cu12]"`), which are additive — CUDA installs + alongside the default provider, not instead of it. +- **One default provider distribution: `transcribe-cpp-native`** (decided + 2026-06-10). Its wheels are platform-tagged and differ in content + (manylinux/win x86_64 → CPU+Vulkan modules; macOS arm64 → Metal); the + descriptor's `backends` field tells the truth per platform, and the + scikit-build-core sdist lives under the same name so "no prebuilt wheel" + falls back to a source build automatically. Specialized providers keep + suffixed names (`transcribe-cpp-native-cu12`). The pyproject for this + package sits at the **repo root** — an sdist cannot reach files outside its + project directory, and the sdist must carry the whole C++ tree. +- **`transcribe-cpp` hard-depends on `transcribe-cpp-native==X.Y.Z.*`** + (decided 2026-06-10): `pip install transcribe-cpp` transcribes out of the + box, which is what the clean-machine gate demands. The `.*` pin is the + pre-1.0 base-version contract at resolver level; the import-time + version/header-hash check stays as the runtime backstop, and + `check_version_sync.py` gates the pin against `transcribe.h`. Dev/CI uv + environments neutralize the dependency via `tool.uv.override-dependencies` + with a provably-false marker (they test locally built libraries, and the + provider isn't on an index until release). +- **Provider discovery via entry points**, group `transcribe_cpp.native`, + not import-name probing. The entry-point contract: provider package name, + native artifact path/directory, native library version, generated + public-header hash, supported backend kinds. The API package hard-fails + before loading a mismatched provider, with an actionable error. Pip pins + are not enough; the runtime check is the backstop. +- **Provider selection ≠ backend request.** Provider selection picks which + native artifact loads into the process; `backend=` maps to + `transcribe_backend_request` inside it. Selection policy: explicit + argument → `TRANSCRIBE_NATIVE_PROVIDER` env var → best accelerated + (CUDA/Metal, then Vulkan) → CPU. An installed provider that cannot satisfy + a backend request raises from the request path — a different error from + "no such provider installed." +- **Versioning.** Pre-1.0, the Python package and native library must match + on the *base* version exactly (post-releases like `0.0.1.post1` must still + load). No version skew support until the API grows a caller-size-aware + init convention. +- **Wheel hygiene.** Official provider wheels: `GGML_NATIVE=OFF`, + OpenMP off, non-Apple system BLAS off (Accelerate is fine on macOS — it is + a system framework, nothing to vendor). Reason: wheel-repair tools vendor + OpenMP/BLAS runtimes that collide with PyTorch/NumPy/MKL in the same + process; `KMP_DUPLICATE_LIB_OK` is not a packaging strategy. Opt-in + OpenMP/BLAS providers only after co-import tests prove coexistence. +- **Linux baseline `manylinux2014`** (matches llama-cpp-python's posture). + Revisit `manylinux_2_28` only if CUDA/Vulkan toolchains force it. +- **Metal ships by default on macOS arm64**, with + `-DGGML_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON` (no sidecar shader files). +- **Native artifacts are named build tuples** (`linux-x86_64-cpu`, + `macos-arm64-metal`, ...). There is no single portable + `libtranscribe.so`; artifacts vary by OS, arch, libc, backend set, and CPU + feature policy. Every wheel is self-contained for its tuple. +- **sdist every release.** Builds from source against vendored ggml; the + universal fallback for platforms/backends without prebuilt wheels and the + path for local CUDA/ROCm/CPU tuning. It does not replace wheels for + adoption. +- **Licensing.** Every wheel vendoring native code includes third-party + license texts (ggml, any bundled backend/runtime components); license + inclusion is part of provider packaging tests. +- **Complete the Python API surface before broadening the provider matrix.** + A mostly complete API on CPU+Metal is a better cross-language precedent + than a broad backend matrix with a partial surface. (The API surface is now + essentially complete, so the remaining work is all distribution.) + +## Remaining design context + +### The provider artifact is a backend-module directory (verified) + +Vulkan-by-default on Linux/Windows forces the artifact-shape decision that +was previously deferred: the default provider is a **directory** of ggml +dynamic backend modules, not one library. The vendored ggml was inspected +(2026-06) and already supports everything required — this is the same shape +llama.cpp ships for its multi-backend release binaries: + +- `GGML_BACKEND_DL=ON` builds every backend as a separate `MODULE` library + (`ggml/src/CMakeLists.txt:247`, `ggml_add_backend_library`). The core + `libggml` **never links `libvulkan`**; only the `libggml-vulkan` module + does. +- The registry tolerates module-load failure: `load_backend()` logs and + returns nullptr when `dlopen` fails (`ggml-backend-reg.cpp:202`). A + machine with no Vulkan loader simply never registers the Vulkan backend — + **no crash, no import failure**, CPU keeps working. +- Loader present but no driver/device is equally safe: + `ggml_backend_vk_reg()` wraps instance init in catch-all handlers and + returns nullptr on any failure (`ggml-vulkan.cpp:15930`); zero devices + logs "No devices found" and returns early (`:5825`). +- `ggml_backend_load_all_from_path(dir)` is the exact primitive for a + package-local artifact directory. +- CUDA implements the identical module protocol + (`GGML_BACKEND_DL_IMPL(ggml_backend_cuda_reg)`), so the CUDA provider + reuses this mechanism unchanged. + +Consequences: + +- The default Linux/Windows provider wheel contains: `libtranscribe` + + `libggml` (+ base libs) + a conservative `ggml-cpu` module + the + `ggml-vulkan` module. Vulkan wins when usable; CPU is the silent floor. +- Packaging risks move into scope and must be proven by the validation + track: `$ORIGIN`/`@loader_path`/`add_dll_directory` resolution inside the + wheel; module search must never escape the package; wheel-repair tools + cannot see string-dlopened modules, so they are explicitly included, + repaired, and re-tested after repair. +- Build images need the Vulkan SDK (glslc) to compile the Vulkan module with + embedded SPIR-V (manylinux container and the Windows lane). + +Degradation validation track (required before release): + +1. Clean Linux container, no loader: import works, CPU transcribes, + `backend="vulkan"` raises a clean Python exception. +2. Container with loader but no ICD/driver: same outcome. +3. Real Vulkan machine (Linux and Windows): `backend="vulkan"` runs and the + diagnostics name the device. + +### CPU variants ("Strategy B") ride the same mechanism — fast follow + +With `GGML_BACKEND_DL` in the release path, fat CPU is no longer a separate +high-risk track: `GGML_CPU_ALL_VARIANTS=ON` is a one-flag addition that +emits one CPU module per ISA tier with runtime feature scoring. It stays a +**fast follow** (not merge-blocking) only because its acceptance gate is +hardware: a no-AVX2 machine must run, an AVX-512 machine must select the +AVX-512 module. Until then the shipped CPU module is the conservative +baseline (every SIMD tier explicitly off — `GGML_NATIVE=OFF` alone does not +disable them). Strategy B+ (compiled-in baseline + dynamic override) remains +an upstream-ggml improvement track. + +### Backend init / diagnostics C API (now required for release) + +The binding must point ggml at the wheel-local module directory, so this +API moves from sketch to requirement — and it is shared groundwork for the +Rust/Swift/TS bindings: + +```c +transcribe_status transcribe_init_backends(const char * artifact_dir); +``` + +Wraps `ggml_backend_load_all_from_path` for a package-local directory; +never searches an ambient system directory. Diagnostics must answer: which +artifact directory loaded, which backend modules/devices were discovered, +which backend serves a given request, and why a requested backend (e.g. +`vulkan`) is unavailable on this machine. Public-header change → regenerate +the FFI layer (the drift gate enforces this). + +### CUDA: opt-in provider, built and tested this cycle + +CUDA stays out of the default wheel for size reasons (fatbins + NVIDIA +runtime deps), not mechanism — it uses the same backend-module shape. +`transcribe-cpp-native-cu12`, installed via `transcribe-cpp[cu12]`. Do not +vendor the toolkit: depend on NVIDIA runtime wheels +(`nvidia-cuda-runtime-cu12`, ...) and resolve/load them explicitly — they +install under `site-packages/nvidia/*/lib`, which the platform loader does +not find unaided. Build CI now; a real NVIDIA runtime smoke is required +before the provider is called release-grade (the one item that may trail +the merge, with explicit sign-off). `cu13`/ROCm only when demand justifies. + +## Punch list + +In order. **The merge bar is release-ready**: everything through Milestone 6 +is merge-blocking (one explicitly flagged exception); Milestone 7 is the +fast-follow set. + +### Milestone 1 — Lock in what exists (CI) + +- [x] GitHub Actions workflow: generator drift gate + (`_generate/generate.py --check`) + version-sync check + (`transcribe.h` ↔ `pyproject.toml` ↔ `__init__.__version__`). + (`.github/workflows/python-bindings.yml`, `lint` job; + `_generate/check_version_sync.py`.) +- [x] Static build lane: full C++ white-box test suite. + (`cpp-tests` job, Linux + macOS.) +- [x] Shared build lane (`TRANSCRIBE_BUILD_SHARED=ON`): `api_smoke` as the + exported-symbol canary + Python no-model tests (import, ABI layout, + version gate). (`python-shared` job.) +- [x] Canary model wired: `handy-computer/whisper-tiny-gguf` (private for + now, public later) → `whisper-tiny-Q5_K_M.gguf` (44 MB, MIT weights). + CI downloads it when the `HF_TOKEN` repo secret exists (forks without + it keep skipping cleanly): `python-shared` runs the offline + transcription tests for real, and `provider-dl-vulkan` tier 2b + transcribes end-to-end ON the lavapipe Vulkan device. The streaming + canary (`handy-computer/moonshine-streaming-tiny-gguf` → Q8_0, 50 MB) + is wired the same way, so the streaming tests run in CI too. The + prompted (nemotron) streaming test stays local-only; its regression + is pinned in CI by the sanitized C-level test. +- [x] Convert `tests/smoke.py` into a pytest suite with proper skips. + +### Milestone 2 — Pre-publish hygiene (small; alongside M1) + +- [x] Raise `requires-python` to ≥3.9 (3.8 is EOL). +- [x] Import-time version gate compares *base* versions so a `.postN` + packaging fix still loads. +- [x] Test asserting `errors.py` status codes match the `_generated` enum + values. + +### Milestone 3 — Correctness floor (done 2026-06-10) + +- [x] Streaming run-params retention fixed **in the dispatcher**, not per + family: `transcribe_stream_begin` copies `language`/`target_language` + into session-owned storage and hands the family hooks a params view + pointing at it, with the run-slot `family` ext nulled (no family reads + it on the stream path; a retained pointer to it would dangle per the + ext copy-out contract). Zero per-family edits. The header documents + the contract: everything passed to begin may be freed once it returns. + Repro evidence: the language-hint pytest failed against the unfixed + library with visibly corrupted memory (`"dn-US"`), and the C-level + repro produced `AddressSanitizer: heap-use-after-free, READ of size 3` + with the fix reverted. Both green post-fix. +- [x] Python `Stream` pins `(run_params, stream_params, ext)` until + `reset()` — defense in depth on top of the C contract. +- [x] `Model.close()` closes live sessions (weakref-tracked) before freeing + the model, so explicit close/context-exit is safe in any order. +- [x] Tests: streaming with `language=` on the prompted parakeet streaming + model + moonshine (`test_streaming.py`); close-ordering/lifetime + battery (`test_lifetime.py`); PCM edge-case battery (`test_pcm.py`, + including optional numpy cases). Suite: 82 passed. +- [x] Sanitizer certification, adjusted for platform reality: the full + white-box C++ suite (38 tests) runs clean under ASan+UBSan, including + a new FFI-shaped regression in `stream_dispatch_unit.cpp` that + heap-allocates params, frees+scribbles them right after begin, and + feeds — the exact ctypes caller shape. A `cpp-tests-sanitized` CI lane + (Linux) now runs this on every push. (Injecting ASan under a Python + process is blocked on macOS — dyld strips `DYLD_INSERT_LIBRARIES`; + the C-level lane certifies the same contracts. A sanitized *pytest* + lane becomes worthwhile once the CI canary model exists.) +- [x] Bonus class fixed while certifying: loading caller-supplied enum + fields through enum-typed lvalues is UB in C++ for out-of-range ints + that C callers can legally pass. Every public boundary now raw-reads + and validates enums first (`enum_field_raw`): run-params + task/timestamps/pnc/itn, session kv_type, model-load backend, + `init_backends`, ABI accessors, `transcribe_model_supports`, stream + commit_policy. UBSan found three live instances; all fixed. + +### Milestone 4 — Backend-module provider core (core done 2026-06-10) + +- [x] `transcribe_init_backends(artifact_dir)` + device diagnostics: + `transcribe_backend_device_count` / `transcribe_get_backend_device` + (name, description, classified kind string) / + `transcribe_backend_available(kind)`. Package-local by construction + (ggml scans ONLY the given dir when one is passed); idempotent per + canonical directory; zero-devices after load is a loud + `ERR_BACKEND`. New ABI id 13; FFI regenerated (header hash moved, as + it should for a real ABI change); covered in `api_smoke` and + `tests/test_backends.py`. Python surface: `backends()`, + `backend_available()`, import-time module loading from the artifact + dir. +- [x] `TRANSCRIBE_GGML_BACKEND_DL` option (requires `TRANSCRIBE_BUILD_SHARED`) + + presets `wheel-linux-cpu-vulkan` and `wheel-windows-cpu-vulkan` + (conservative CPU module + Vulkan module). macOS arm64 stays a single + Metal artifact. **Track finding:** the first DL build exposed direct + link-time references to CPU-module symbols + (`ggml_backend_cpu_init/_set_n_threads` in parakeet's host-joint + decoder) — replaced with registry-based + `ggml_backend_init_by_type` + `get_proc_address`, the pattern any + backend-specific call must use from now on. +- [x] Loader/binding wiring: the loader records the artifact directory + (provider `artifact_dir`, else the library's own dir), import calls + `transcribe_init_backends` on it, Windows already uses + `os.add_dll_directory`. +- [x] **Local end-to-end proof (macOS):** built the DL configuration, + assembled a flat wheel-like directory (`libtranscribe` + ggml libs + + `libggml-cpu.so` + `libggml-metal.so`, `@loader_path` rpaths via + `install_name_tool`), hid the build tree so nothing could resolve + outside it, and ran the full 90-test pytest suite through it — ggml + logged both modules loading from the provider directory, devices + classified `[('MTL0','metal'), ('CPU','cpu')]`, vulkan answered + unavailable, and a real model transcribed on Metal. +- [x] CI `provider-dl-vulkan` lane, **green 2026-06-10**: builds the Linux + CPU+Vulkan module provider with glslc, assembles the flat directory + with `$ORIGIN` rpaths via patchelf, deletes the build tree, then + proves tier 1 (Vulkan loader removed → `[('CPU','cpu')]`, vulkan + answers False) and tier 2 (mesa lavapipe installed → the same + artifacts report `[('Vulkan0','vulkan'), ('CPU','cpu')]`; needs + `GGML_VK_VISIBLE_DEVICES=0` because lavapipe is a CPU-type Vulkan + device that ggml's default dedicated-GPU filter excludes). Getting it + green also caught: the `GGML_NATIVE`+`BACKEND_DL` x86 hard error (now + forced off by `TRANSCRIBE_GGML_BACKEND_DL`) and gcc's internal + linkage for `extern "C"` definitions inside anonymous namespaces + (clang exports them — the new API had to move to file scope). +- [x] Real-GPU Vulkan validation, Linux (2026-06-10): Fedora / ThinkPad + T14, AMD Renoir iGPU via RADV. Provider-directory build assembled on + the machine, `libggml-vulkan.so` loaded from the directory, device + discovered through the DEFAULT selection path (no + `GGML_VK_VISIBLE_DEVICES` override — confirms integrated GPUs pass + ggml's device filter), model placed on `Vulkan0`, jfk.wav transcribed + correctly with `backend="vulkan"`. +- [ ] Remaining: real-GPU Vulkan validation on a physical **Windows** + machine, and Vulkan SDK in the *cibuildwheel* images when the M5 + wheel lanes are built. + +### Milestone 5 — Wheels and out-of-box proof + +- [x] Provider discovery in `_library.py`: entry points in group + `transcribe_cpp.native`, version/header-hash hard-fail per the + contract above. (Descriptor fields: `name`, `library_path`/`artifact_dir`, + `version`, `header_hash`, `backends`. Selection: explicit arg → + `TRANSCRIBE_NATIVE_PROVIDER` → best accelerated → CPU. The ABI tag is + `_generated.PUBLIC_HEADER_HASH`, a stable digest the generator emits; + a provider echoes it back. Covered by `tests/test_provider_discovery.py`.) +- [x] End-to-end provider test (fake entry point → descriptor → selection → + contract → load): `test_provider_discovery.py` now builds a real + installed-distribution shape on disk (module + `.dist-info` advertising + `transcribe_cpp.native`) and drives importlib.metadata discovery → + `ep.load()` → contract → an actual dlopen of the suite's library; the + ABI-mismatch twin proves refusal happens before dlopen. +- [x] Provider package skeleton (done 2026-06-10): `transcribe-cpp-native` + builds from the repo-root `pyproject.toml` via **scikit-build-core** + (one backend for wheels AND the from-source sdist). Pieces: + `cmake/python-wheel-install.cmake` (SKBUILD-only; flat + `transcribe_cpp_native/_native/` artifact dir, `COMPONENT wheel` so + ggml's header/cmake-config installs stay out, `$ORIGIN`/`@loader_path` + rpaths, VERSION/SOVERSION stripped — wheels can't carry symlinks — and + `_contract.py` stamped from `PROJECT_VERSION` + the drift-gated + `_generated.py` header hash + `GGML_AVAILABLE_BACKENDS`); + `bindings/python-native/` (descriptor + entry point); SKBUILD posture + block in the top-level CMakeLists (shared, no tests/examples, OpenMP/ + system-BLAS off by default, Metal shaders embedded). Package version is + regex-parsed from `transcribe.h` at build time — same single source as + CMake. `[tool.uv] package = false` keeps `uv run scripts/...` from ever + triggering a surprise native build (verified). + **Local end-to-end proof (macOS arm64):** `uv build --wheel` → + `transcribe_cpp_native-0.0.1-py3-none-macosx_26_0_arm64.whl` (3.4 MB: + libtranscribe + ggml/base/cpu/metal, `_contract.py` echoing hash + `0007af60bfcecf7e`, backends `("metal","cpu")`, both LICENSE files); + otool shows every cross-ref `@rpath` + `@loader_path` with system-only + externals; clean venv + `pip install` both wheels → entry-point + discovery selected the provider from site-packages, Metal device found, + embedded metallib used, jfk.wav transcribed, **zero env vars**; full + pytest suite (95 tests) green against the installed pair AND in + dev-tree mode. +- [ ] cibuildwheel lanes: Linux x86_64 (cpu+vulkan), Windows x86_64 + (cpu+vulkan), macOS arm64 (Metal). Repair with + auditwheel/delvewheel/delocate — string-dlopened modules must be + explicitly included — and **test the repaired artifacts**, not the + build outputs. +- [ ] sdist that compiles from source — the scikit-build-core backend and + root pyproject (with size excludes) exist per the skeleton above; + remaining: build the sdist, audit its contents/size, and prove + `pip install ` compiles and transcribes on a clean machine. +- [ ] Co-import smokes in wheel CI: `numpy` then `torch`, each followed by a + real transcription. +- [ ] Clean-machine installs (no repo checkout, no env vars): fresh + manylinux container, clean macOS account/machine, clean Windows + machine — install the wheel, transcribe. +- [ ] TestPyPI rehearsal of the full set; install-from-index smoke; then + merge and cut **0.1.0 on PyPI** from the merge commit. + +### Milestone 6 — CUDA provider (this cycle) + +- [ ] `transcribe-cpp-native-cu12` package: same backend-module mechanism, + NVIDIA runtime-wheel dependencies, explicit load-path resolution for + `site-packages/nvidia/*/lib`. +- [ ] Build CI lane. +- [ ] Real NVIDIA runtime smoke. *The one item that may trail the merge, + with explicit sign-off — it requires GPU hardware/runners.* + +### Milestone 7 — Fast follows (post-merge) + +- [ ] Fat CPU variants: add `GGML_CPU_ALL_VARIANTS=ON` to the proven module + directory; gate on no-AVX2 and AVX-512 hardware validation; then make + it the default CPU module set. +- [ ] README/examples polish round two; `cu13`/ROCm if demand justifies; + Strategy B+ upstream patch evaluation. + +## Open questions + +- Whether the CUDA runtime smoke gates the merge or trails it (needs NVIDIA + hardware — human decision). +- Which physical machines validate Vulkan-on-Windows and the fat-CPU + hardware matrix. +- Whether to pursue (and upstream) the Strategy B+ baseline-plus-variants + ggml patch. +- Whether opt-in OpenMP/BLAS-enabled providers are worth shipping after + co-import testing. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..053fd7ef --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,99 @@ +# transcribe-cpp-native: the default native provider package for the +# transcribe-cpp Python bindings (see docs/python-bindings-distribution-plan.md). +# +# This pyproject lives at the repo root on purpose: the sdist must contain the +# full C++ tree (src/, include/, vendored ggml/) so `pip install` can compile +# from source on platforms without a prebuilt wheel, and an sdist cannot reach +# files outside its project directory. The pure-Python API package +# (transcribe-cpp) lives at bindings/python and depends on this one. +# +# One distribution name, platform-differentiated wheels: +# manylinux / win -> conservative CPU + Vulkan ggml backend modules +# macOS arm64 -> Metal (embedded shaders) + CPU +# sdist -> builds from source; machine-tuned CPU by default +# +# The wheel is py3-none-: the package contains zero compiled Python +# (the binding is ctypes), only the native artifact directory. + +[build-system] +requires = ["scikit-build-core>=0.11"] +build-backend = "scikit_build_core.build" + +[project] +name = "transcribe-cpp-native" +dynamic = ["version"] +description = "Prebuilt native library for transcribe-cpp (speech-to-text, ggml)" +readme = "bindings/python-native/README.md" +requires-python = ">=3.9" +license = "MIT" +# Wheels vendor native code, so third-party license texts ship in the wheel +# (settled decision: license inclusion is part of provider packaging tests). +license-files = ["LICENSE", "ggml/LICENSE"] +authors = [{ name = "The transcribe.cpp authors" }] +keywords = ["transcription", "speech-to-text", "asr", "ggml", "native"] +classifiers = [ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] + +[project.urls] +Homepage = "https://github.com/handy-computer/transcribe.cpp" +Repository = "https://github.com/handy-computer/transcribe.cpp" +Issues = "https://github.com/handy-computer/transcribe.cpp/issues" + +# The provider contract: transcribe_cpp discovers native artifacts through this +# entry-point group and validates version + public-header hash before dlopen +# (bindings/python/src/transcribe_cpp/_library.py). +[project.entry-points."transcribe_cpp.native"] +default = "transcribe_cpp_native:descriptor" + +[tool.scikit-build] +# The version is parsed from the TRANSCRIBE_VERSION_* macros in the public +# header — the same single source CMake and transcribe_version() use — so the +# provider package version cannot drift from the native library it bundles. +metadata.version.provider = "scikit_build_core.metadata.regex" +metadata.version.input = "include/transcribe.h" +metadata.version.regex = '(?sx) TRANSCRIBE_VERSION_MAJOR \s+ (?P\d+) .*? TRANSCRIBE_VERSION_MINOR \s+ (?P\d+) .*? TRANSCRIBE_VERSION_PATCH \s+ (?P\d+)' +metadata.version.result = "{major}.{minor}.{patch}" + +cmake.version = ">=3.21" +ninja.version = ">=1.11" + +# ctypes binding: no compiled Python, one wheel per platform spans all CPythons. +wheel.py-api = "py3" +# CMake's install prefix maps inside the import package, so the artifact +# directory lands at transcribe_cpp_native/_native/. +wheel.install-dir = "transcribe_cpp_native" +wheel.packages = ["bindings/python-native/transcribe_cpp_native"] + +# Package ONLY the wheel component (cmake/python-wheel-install.cmake): the +# vendored ggml's own install rules (headers, cmake config, pkg-config) use the +# default component and must stay out of the wheel. +install.components = ["wheel"] + +# Keep the sdist to the sources a from-scratch build needs. Converted models, +# dumps, perf/WER reports, and local build trees are multi-GB and never belong +# in a distribution. +sdist.exclude = [ + "models/", + "dumps/", + "reports/", + "tmp/", + "build*/", + "dist*/", + ".github/", + ".claude/", + "bindings/python/dist/", + "**/__pycache__/", + "**/.venv/", +] + +[tool.uv] +# Keep `uv run