1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// transcribe-loader.cpp - implementation of the GGUF loader.
//
// See transcribe-loader.h for the contract. This file is intentionally
// architecture-agnostic: it knows nothing about Parakeet, Whisper, or any
// other family. It only reads enough KV to identify which family handler
// the per-arch dispatch should hand the file to.
#include "transcribe-loader.h"
#include "gguf.h"
#include "transcribe-meta.h"
#include <sys/stat.h>
#include <cerrno>
namespace transcribe {
Loader::~Loader() {
if (gguf_ != nullptr) {
gguf_free(gguf_);
gguf_ = nullptr;
}
}
gguf_context * Loader::release_gguf() {
gguf_context * out = gguf_;
gguf_ = nullptr;
return out;
}
namespace {
// Narrow stat-based pre-check whose ONLY job is to distinguish
// "the file is not at this path" from every other reason
// gguf_init_from_file might return nullptr.
//
// We deliberately do not check S_ISREG here. If stat() succeeds and the
// path points at a directory (or a fifo, or a socket), the file IS at
// that path — it's just not a usable GGUF. fopen() inside ggml will
// return EISDIR / etc. and we will surface that as TRANSCRIBE_ERR_GGUF,
// which is strictly more accurate than collapsing it into FILE_NOT_FOUND.
//
// We also deliberately only short-circuit on ENOENT / ENOTDIR. Other
// stat() failures (EACCES, ENAMETOOLONG, ELOOP, EIO, ...) do NOT
// conclusively prove the file is missing — they prove we can't reach
// it. Returning FILE_NOT_FOUND for those would be wrong. We let
// gguf_init_from_file try and surface whatever it returns as ERR_GGUF.
//
// Returns true if the path either exists OR if we cannot prove it
// doesn't (in which case the caller should hand off to
// gguf_init_from_file unchanged).
bool path_is_present(const char * path) {
struct stat st{};
if (::stat(path, &st) == 0) {
return true;
}
// ENOENT: the named file does not exist.
// ENOTDIR: a non-final component of the path is not a directory,
// which is the same observable failure mode as "the file
// is not at this path."
if (errno == ENOENT || errno == ENOTDIR) {
return false;
}
return true;
}
} // namespace
transcribe_status Loader::open(const char * path) {
if (path == nullptr) {
return TRANSCRIBE_ERR_INVALID_ARG;
}
path_ = path;
if (!path_is_present(path)) {
return TRANSCRIBE_ERR_FILE_NOT_FOUND;
}
// Header-only inspection: do not allocate ggml tensors. With ctx=null
// gguf_init_from_file skips the entire tensor-allocation block; with
// no_alloc=true any future code path that does pass a ctx will not
// read the data blob.
gguf_init_params init_params{};
init_params.no_alloc = true;
init_params.ctx = nullptr;
gguf_ = gguf_init_from_file(path, init_params);
if (gguf_ == nullptr) {
// ggml has already logged a structured error via GGML_LOG_ERROR
// (corrupt magic, version mismatch, truncated header, IO error
// after the existence check, etc.). All of those collapse to a
// single public status.
return TRANSCRIBE_ERR_GGUF;
}
// general.architecture is required. Without it the dispatch layer
// has nothing to look up in the registry. Both Absent and BadType
// are fatal: a missing arch and an arch with the wrong GGUF type
// both leave us with no family to dispatch to.
switch (read_string_kv(gguf_, "general.architecture", arch_)) {
case KvResult::Absent:
case KvResult::BadType:
return TRANSCRIBE_ERR_GGUF;
case KvResult::Ok:
break;
}
// stt.variant is optional in 0.x. Per-family handlers default it
// when absent (e.g. parakeet -> tdt-0.6b-v2). A present-but-wrong-
// type variant is still a converter bug, though, so BadType is
// fatal here while Absent silently leaves variant_ empty.
switch (read_string_kv(gguf_, "stt.variant", variant_)) {
case KvResult::Absent:
// variant_ remains empty; family handler will default it.
break;
case KvResult::BadType:
return TRANSCRIBE_ERR_GGUF;
case KvResult::Ok:
break;
}
// Copy every scalar-string KV into the metadata map. This mirrors
// llama.cpp's generic metadata accessor: the public API exposes one
// keyed getter (transcribe_model_meta_val_str) instead of a typed
// accessor per field, so adding a new string KV in the converter needs
// no API change. Non-string KVs (hparams, arrays such as the token
// list) are intentionally skipped — this is identity/display metadata.
const int64_t n_kv = gguf_get_n_kv(gguf_);
for (int64_t i = 0; i < n_kv; ++i) {
if (gguf_get_kv_type(gguf_, i) != GGUF_TYPE_STRING) {
continue;
}
meta_.emplace(gguf_get_key(gguf_, i), gguf_get_val_str(gguf_, i));
}
return TRANSCRIBE_OK;
}
} // namespace transcribe