arcane-os 0.62.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +7 -2
- package/docs/reference/README.md +7 -0
- package/docs/reference/cli.md +5 -1
- package/docs/reference/core-client.md +11 -2
- package/docs/reference/core-native-packaging.md +57 -8
- package/docs/reference/core-repositories.md +149 -0
- package/docs/reference/inventory/package-api.json +163 -9
- package/docs/reference/local-ai.md +51 -7
- package/docs/reference/local-image-flux.md +154 -0
- package/docs/reference/local-image-generation.md +12 -0
- package/docs/reference/local-image-sdxl.md +139 -0
- package/docs/reference/local-whisper.md +219 -0
- package/docs/reference/native-speech.md +163 -0
- package/docs/reference/runtime-modules.md +16 -2
- package/docs/reference/sdk-api.md +335 -6
- package/package.json +5 -1
- package/runtime/arcane/components/speech.html +15 -3
- package/runtime/arcane/components/voice-transcription.html +19 -3
- package/runtime/arcane/modules/AI.js +250 -27
- package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
- package/runtime/arcane/modules/AIRuntimeState.js +15 -2
- package/runtime/arcane/modules/ComponentContracts.js +3 -0
- package/schemas/arcane-app.schema.json +53 -2
- package/schemas/arcane-package.schema.json +53 -2
- package/src/core/repositories.mjs +210 -0
- package/src/core/services/speech.mjs +361 -0
- package/src/local-ai/config.mjs +9 -3
- package/src/local-ai/dev.mjs +32 -2
- package/src/local-ai/image-library.mjs +9 -0
- package/src/local-ai/image-models/flux.mjs +31 -0
- package/src/local-ai/image-models/sdxl.mjs +18 -0
- package/src/local-ai/install.mjs +26 -7
- package/src/local-ai/whisper/build.mjs +48 -0
- package/src/local-ai/whisper/config.mjs +61 -0
- package/src/local-ai/whisper/index.mjs +515 -0
- package/src/local-ai/whisper/install.mjs +274 -0
- package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
- package/src/local-ai/whisper/native/NOTICE +18 -0
- package/src/local-ai/whisper/native/README.md +104 -0
- package/src/local-ai/whisper/native/main.cpp +489 -0
- package/src/native/portable-layout.mjs +37 -7
- package/src/native/windows-provider.mjs +39 -15
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {createWriteStream} from 'node:fs';
|
|
2
|
+
import {copyFile, cp, mkdir, readdir, stat, unlink} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {Readable} from 'node:stream';
|
|
5
|
+
import {pipeline} from 'node:stream/promises';
|
|
6
|
+
import Is from 'strong-type';
|
|
7
|
+
import packageMetadata from '../../../package.json' with {type: 'json'};
|
|
8
|
+
import {ArcaneError, ERROR_CODES, throwIfAborted} from '../../errors.mjs';
|
|
9
|
+
import {extractLocalAIArchive} from '../archive.mjs';
|
|
10
|
+
|
|
11
|
+
const is = new Is(false);
|
|
12
|
+
|
|
13
|
+
/** Functional selection keys share preparation, never identify model content. */
|
|
14
|
+
export function whisperRuntimeSelection(requirement, includeModels = true) {
|
|
15
|
+
const {models, modelId, helperRoot, helperVersion, ...runtime} = requirement;
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
...runtime,
|
|
18
|
+
...(includeModels ? {models, modelId} : {}),
|
|
19
|
+
helperVersion: helperVersion ?? packageMetadata.version,
|
|
20
|
+
helperRoot: helperRoot ?? (is.string(requirement.helperExecutable) ? path.dirname(requirement.helperExecutable) : undefined)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function installedWhisperRuntime(record, signal, includeModels = true) {
|
|
25
|
+
if (!is.array(record?.variants) || record.variants.length === 0 || !is.array(record.models)) return false;
|
|
26
|
+
const files = [record.helperExecutable, record.decoderExecutable];
|
|
27
|
+
const directories = [record.root, record.helperRoot ?? (is.string(record.helperExecutable) ? path.dirname(record.helperExecutable) : undefined), record.decoderRoot];
|
|
28
|
+
for (const variant of record.variants) {
|
|
29
|
+
files.push(variant.executable);
|
|
30
|
+
directories.push(variant.root, variant.libraryDirectory);
|
|
31
|
+
}
|
|
32
|
+
if (includeModels) for (const model of record.models) files.push(model.path);
|
|
33
|
+
for (const location of directories) {
|
|
34
|
+
throwIfAborted(signal);
|
|
35
|
+
if (!is.string(location) || !(await stat(location)).isDirectory()) return false;
|
|
36
|
+
}
|
|
37
|
+
for (const location of files) {
|
|
38
|
+
throwIfAborted(signal);
|
|
39
|
+
if (!is.string(location) || !(await stat(location)).isFile()) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function findWhisperFile(root, filename, signal) {
|
|
45
|
+
const directories = [root];
|
|
46
|
+
for (const directory of directories) {
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
for (const entry of await readdir(directory, {withFileTypes: true})) {
|
|
49
|
+
const location = path.join(directory, entry.name);
|
|
50
|
+
if (entry.name === filename && (entry.isFile() || entry.isSymbolicLink())) return location;
|
|
51
|
+
if (entry.isDirectory()) directories.push(location);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, `The selected Whisper dependency tree does not contain ${filename}.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function downloadWhisperResource(url, filename, {signal, onEvent, upstreamResponse}) {
|
|
58
|
+
await onEvent({
|
|
59
|
+
type: 'local-ai.install.downloading',
|
|
60
|
+
message: `Downloading ${path.basename(filename)}.`,
|
|
61
|
+
data: {id: 'whisper.cpp', asset: path.basename(filename)}
|
|
62
|
+
});
|
|
63
|
+
const response = await upstreamResponse(url, signal);
|
|
64
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(filename, {flags: 'wx'}), {signal});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function prepareWhisperArchive(url, root, options) {
|
|
68
|
+
const filename = path.posix.basename(new URL(url).pathname);
|
|
69
|
+
const archive = path.join(path.dirname(root), `${path.basename(root)}-${filename}`);
|
|
70
|
+
await downloadWhisperResource(url, archive, options);
|
|
71
|
+
await options.onEvent({
|
|
72
|
+
type: 'local-ai.install.extracting',
|
|
73
|
+
message: `Extracting ${filename}.`,
|
|
74
|
+
data: {id: 'whisper.cpp', asset: filename}
|
|
75
|
+
});
|
|
76
|
+
await extractLocalAIArchive({archive, directory: root, signal: options.signal, onEvent: options.onEvent});
|
|
77
|
+
await unlink(archive);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function copyWhisperTree(source, destination, signal) {
|
|
81
|
+
throwIfAborted(signal);
|
|
82
|
+
await cp(path.resolve(source), destination, {
|
|
83
|
+
recursive: true,
|
|
84
|
+
verbatimSymlinks: true,
|
|
85
|
+
filter: function retainCompleteWhisperTree() {
|
|
86
|
+
throwIfAborted(signal);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
throwIfAborted(signal);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function copiedWhisperLocation(sourceRoot, destinationRoot, location) {
|
|
94
|
+
const root = path.resolve(sourceRoot);
|
|
95
|
+
const relative = path.relative(root, path.resolve(root, location));
|
|
96
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
97
|
+
throw new ArcaneError(ERROR_CODES.usage, 'A Whisper library, helper, or decoder path must belong to its selected complete runtime tree so the native bundle can retain it.');
|
|
98
|
+
}
|
|
99
|
+
return path.join(destinationRoot, relative);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultWhisperVariants(requirement, platform, architecture) {
|
|
103
|
+
if (requirement.variants) return requirement.variants;
|
|
104
|
+
if (platform !== 'win32' || architecture !== 'x64' || requirement.version !== '1.9.4') {
|
|
105
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Automatic Whisper distributions are available for 1.9.4 on win32/x64. Supply complete runtime variants for ${requirement.version} on ${platform}/${architecture}.`);
|
|
106
|
+
}
|
|
107
|
+
if (requirement.backend === 'metal') {
|
|
108
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The selected Windows Whisper distribution has no Metal backend.');
|
|
109
|
+
}
|
|
110
|
+
return [
|
|
111
|
+
...(requirement.backend === 'cpu' ? [] : [{
|
|
112
|
+
backend: 'cuda',
|
|
113
|
+
url: 'https://github.com/ggml-org/whisper.cpp/releases/download/b5130/whisper-cublas-12.4.0-bin-x64.zip'
|
|
114
|
+
}]),
|
|
115
|
+
{
|
|
116
|
+
backend: 'cpu',
|
|
117
|
+
url: 'https://github.com/ggml-org/whisper.cpp/releases/download/b5130/whisper-bin-x64.zip'
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Prepare complete runtime trees under the shared install attempt owner. */
|
|
123
|
+
export async function installWhisperDistribution(requirement, options) {
|
|
124
|
+
const {root, platform, architecture, onEvent, reusable} = options;
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
127
|
+
const preparation = {...options, signal};
|
|
128
|
+
const variants = reusable?.runtime.variants ?? defaultWhisperVariants(requirement, platform, architecture);
|
|
129
|
+
if (!requirement.helperExecutable && !requirement.helperUrl && (platform !== 'win32' || architecture !== 'x64')) {
|
|
130
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply the Arcane Whisper helperExecutable or helperUrl for ${platform}/${architecture}.`);
|
|
131
|
+
}
|
|
132
|
+
if (!requirement.decoderExecutable && !requirement.decoderUrl && (platform !== 'win32' || architecture !== 'x64')) {
|
|
133
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply a complete decoder tree through decoderExecutable and decoderRoot, or decoderUrl, for ${platform}/${architecture}.`);
|
|
134
|
+
}
|
|
135
|
+
const helperName = `arcane-whisper${platform === 'win32' ? '.exe' : ''}`;
|
|
136
|
+
const helperRoot = path.join(root, 'helper');
|
|
137
|
+
let helperExecutable = path.join(helperRoot, helperName);
|
|
138
|
+
const decoderRoot = path.join(root, 'decoder');
|
|
139
|
+
const models = [];
|
|
140
|
+
const preparedVariants = [];
|
|
141
|
+
let decoderExecutable;
|
|
142
|
+
await mkdir(root, {recursive: true});
|
|
143
|
+
|
|
144
|
+
const jobs = variants.map(function variantPreparation(variant, index) {
|
|
145
|
+
return async function prepareSelectedVariant() {
|
|
146
|
+
const variantRoot = path.join(root, `variant-${index}-${variant.backend}`);
|
|
147
|
+
const selectedLibraryDirectory = variant.libraryDirectory
|
|
148
|
+
? copiedWhisperLocation(variant.root ?? variantRoot, variantRoot, variant.libraryDirectory)
|
|
149
|
+
: undefined;
|
|
150
|
+
if (variant.root) await copyWhisperTree(variant.root, variantRoot, signal);
|
|
151
|
+
else await prepareWhisperArchive(variant.url, variantRoot, preparation);
|
|
152
|
+
const libraryName = platform === 'win32' ? 'whisper.dll' : platform === 'darwin' ? 'libwhisper.dylib' : 'libwhisper.so';
|
|
153
|
+
const libraryDirectory = selectedLibraryDirectory ?? path.dirname(await findWhisperFile(variantRoot, libraryName, signal));
|
|
154
|
+
preparedVariants[index] = {backend: variant.backend, root: variantRoot, libraryDirectory};
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
jobs.push(async function prepareHelper() {
|
|
158
|
+
const existingHelper = reusable?.runtime.helperExecutable ?? requirement.helperExecutable;
|
|
159
|
+
if (existingHelper) {
|
|
160
|
+
const source = path.resolve(existingHelper);
|
|
161
|
+
const sourceRoot = path.resolve(reusable
|
|
162
|
+
? reusable.runtime.helperRoot ?? path.dirname(source)
|
|
163
|
+
: requirement.helperRoot ?? path.dirname(source));
|
|
164
|
+
helperExecutable = copiedWhisperLocation(sourceRoot, helperRoot, source);
|
|
165
|
+
await copyWhisperTree(sourceRoot, helperRoot, signal);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const helperVersion = requirement.helperVersion ?? packageMetadata.version;
|
|
169
|
+
const helperUrl = requirement.helperUrl ?? `https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${helperVersion}/arcane-whisper-windows-x64.tar.gz`;
|
|
170
|
+
await prepareWhisperArchive(helperUrl, helperRoot, preparation);
|
|
171
|
+
helperExecutable = await findWhisperFile(helperRoot, helperName, signal);
|
|
172
|
+
});
|
|
173
|
+
jobs.push(async function prepareDecoder() {
|
|
174
|
+
const existingDecoder = reusable?.runtime.decoderExecutable ?? requirement.decoderExecutable;
|
|
175
|
+
if (existingDecoder) {
|
|
176
|
+
const source = path.resolve(existingDecoder);
|
|
177
|
+
const sourceRoot = path.resolve(reusable?.runtime.decoderRoot ?? requirement.decoderRoot ?? path.dirname(source));
|
|
178
|
+
decoderExecutable = copiedWhisperLocation(sourceRoot, decoderRoot, source);
|
|
179
|
+
await copyWhisperTree(sourceRoot, decoderRoot, signal);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await prepareWhisperArchive(
|
|
183
|
+
requirement.decoderUrl ?? 'https://github.com/GyanD/codexffmpeg/releases/download/9.0.2/ffmpeg-9.0.2-essentials_build.zip',
|
|
184
|
+
decoderRoot,
|
|
185
|
+
preparation
|
|
186
|
+
);
|
|
187
|
+
decoderExecutable = await findWhisperFile(decoderRoot, `ffmpeg${platform === 'win32' ? '.exe' : ''}`, signal);
|
|
188
|
+
});
|
|
189
|
+
for (const [index, model] of requirement.models.entries()) {
|
|
190
|
+
jobs.push(async function prepareSelectedModel() {
|
|
191
|
+
const modelRoot = path.join(root, 'models', String(index));
|
|
192
|
+
await mkdir(modelRoot, {recursive: true});
|
|
193
|
+
const filename = model.path ? path.basename(model.path) : model.filename ?? path.posix.basename(new URL(model.url).pathname);
|
|
194
|
+
const destination = path.join(modelRoot, filename);
|
|
195
|
+
const priorIndex = reusable?.requirement.models.findIndex(function sameSelectedModel(candidate) {
|
|
196
|
+
return JSON.stringify(candidate) === JSON.stringify(model);
|
|
197
|
+
}) ?? -1;
|
|
198
|
+
let source = model.path;
|
|
199
|
+
if (priorIndex >= 0) {
|
|
200
|
+
const priorPath = reusable.runtime.models[priorIndex]?.path;
|
|
201
|
+
if (priorPath) {
|
|
202
|
+
try {
|
|
203
|
+
if ((await stat(priorPath)).isFile()) source = priorPath;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error.code !== 'ENOENT') throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (source) await copyFile(path.resolve(source), destination);
|
|
210
|
+
else await downloadWhisperResource(model.url, destination, preparation);
|
|
211
|
+
models[index] = {...model, path: destination};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
let nextJob = 0;
|
|
215
|
+
const failures = [];
|
|
216
|
+
async function prepareWhisperDependencies() {
|
|
217
|
+
while (nextJob < jobs.length) {
|
|
218
|
+
throwIfAborted(signal);
|
|
219
|
+
const job = jobs[nextJob++];
|
|
220
|
+
try {
|
|
221
|
+
await job();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
failures.push(error);
|
|
224
|
+
controller.abort(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const workers = await Promise.allSettled(Array.from({length: Math.min(3, jobs.length)}, function startWhisperDependencyWorker() {
|
|
229
|
+
return prepareWhisperDependencies();
|
|
230
|
+
}));
|
|
231
|
+
for (const worker of workers) if (worker.status === 'rejected') failures.push(worker.reason);
|
|
232
|
+
throwIfAborted(options.signal);
|
|
233
|
+
if (failures.length === 1) throw failures[0];
|
|
234
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Whisper dependency preparation failed.');
|
|
235
|
+
const locations = {
|
|
236
|
+
version: requirement.version,
|
|
237
|
+
backend: requirement.backend,
|
|
238
|
+
helperRoot,
|
|
239
|
+
helperExecutable,
|
|
240
|
+
helperVersion: requirement.helperVersion ?? packageMetadata.version,
|
|
241
|
+
decoderRoot,
|
|
242
|
+
decoderExecutable,
|
|
243
|
+
variants: preparedVariants.map(function completedWhisperVariant(variant) {
|
|
244
|
+
return {...variant, executable: helperExecutable};
|
|
245
|
+
}),
|
|
246
|
+
models,
|
|
247
|
+
...(requirement.modelId ? {modelId: requirement.modelId} : models.length === 1 ? {modelId: models[0].id} : {})
|
|
248
|
+
};
|
|
249
|
+
if (!await installedWhisperRuntime({...locations, root}, signal)) {
|
|
250
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, 'Whisper preparation did not produce the selected helper, decoder, runtime directories, and model files.');
|
|
251
|
+
}
|
|
252
|
+
await onEvent({type: 'local-ai.install.prepared', message: 'Whisper runtime, decoder, helper, and selected models are prepared.', data: {id: 'whisper.cpp'}});
|
|
253
|
+
return locations;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function bundledWhisperRuntime(runtime, relativeRoot) {
|
|
257
|
+
function bundledWhisperPath(location) {
|
|
258
|
+
return path.posix.join(relativeRoot, path.relative(runtime.root, location).split(path.sep).join('/'));
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...runtime,
|
|
262
|
+
root: relativeRoot,
|
|
263
|
+
helperRoot: bundledWhisperPath(runtime.helperRoot ?? path.dirname(runtime.helperExecutable)),
|
|
264
|
+
helperExecutable: bundledWhisperPath(runtime.helperExecutable),
|
|
265
|
+
decoderRoot: bundledWhisperPath(runtime.decoderRoot),
|
|
266
|
+
decoderExecutable: bundledWhisperPath(runtime.decoderExecutable),
|
|
267
|
+
variants: runtime.variants.map(function bundledWhisperVariant(variant) {
|
|
268
|
+
return {...variant, root: bundledWhisperPath(variant.root), libraryDirectory: bundledWhisperPath(variant.libraryDirectory), executable: bundledWhisperPath(variant.executable)};
|
|
269
|
+
}),
|
|
270
|
+
models: runtime.models.map(function bundledWhisperModel(model) {
|
|
271
|
+
return {...model, path: bundledWhisperPath(model.path)};
|
|
272
|
+
})
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.26)
|
|
2
|
+
project(ArcaneWhisper LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(WHISPER_SOURCE_DIRECTORY "" CACHE PATH "Matching upstream whisper.cpp source directory")
|
|
5
|
+
set(WHISPER_RUNTIME_DIRECTORY "" CACHE PATH "Selected complete upstream runtime directory")
|
|
6
|
+
|
|
7
|
+
find_path(WHISPER_INCLUDE_DIRECTORY whisper.h
|
|
8
|
+
PATHS "${WHISPER_SOURCE_DIRECTORY}/include" REQUIRED NO_DEFAULT_PATH)
|
|
9
|
+
find_path(GGML_INCLUDE_DIRECTORY ggml-backend.h
|
|
10
|
+
PATHS "${WHISPER_SOURCE_DIRECTORY}/ggml/include" REQUIRED NO_DEFAULT_PATH)
|
|
11
|
+
|
|
12
|
+
function(import_whisper_library name)
|
|
13
|
+
if(WIN32)
|
|
14
|
+
find_file(${name}_DLL "${name}.dll"
|
|
15
|
+
PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/bin"
|
|
16
|
+
REQUIRED NO_DEFAULT_PATH)
|
|
17
|
+
find_library(${name}_IMPORT_LIBRARY NAMES "${name}"
|
|
18
|
+
PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/lib"
|
|
19
|
+
NO_DEFAULT_PATH)
|
|
20
|
+
if(NOT ${name}_IMPORT_LIBRARY)
|
|
21
|
+
if(NOT MSVC)
|
|
22
|
+
message(FATAL_ERROR "This upstream Windows runtime needs MSVC to generate its missing import library.")
|
|
23
|
+
endif()
|
|
24
|
+
get_filename_component(compiler_directory "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
|
25
|
+
find_program(DUMPBIN_EXECUTABLE dumpbin HINTS "${compiler_directory}" REQUIRED)
|
|
26
|
+
find_program(LIB_EXECUTABLE lib HINTS "${compiler_directory}" REQUIRED)
|
|
27
|
+
execute_process(COMMAND "${DUMPBIN_EXECUTABLE}" /NOLOGO /EXPORTS "${${name}_DLL}"
|
|
28
|
+
OUTPUT_VARIABLE export_table ERROR_VARIABLE export_error RESULT_VARIABLE export_result)
|
|
29
|
+
if(NOT export_result EQUAL 0)
|
|
30
|
+
message(FATAL_ERROR "${export_table}${export_error}")
|
|
31
|
+
endif()
|
|
32
|
+
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/${name}-exports.txt" "${export_table}${export_error}")
|
|
33
|
+
string(REGEX MATCHALL "[\r\n][ \t]+[0-9]+[ \t]+[0-9A-Fa-f]+[ \t]+[0-9A-Fa-f]+[ \t]+[^ \t\r\n]+" rows "${export_table}")
|
|
34
|
+
set(definition "LIBRARY ${name}.dll\nEXPORTS\n")
|
|
35
|
+
foreach(row IN LISTS rows)
|
|
36
|
+
string(REGEX REPLACE "^[\r\n][ \t]+[0-9]+[ \t]+[0-9A-Fa-f]+[ \t]+[0-9A-Fa-f]+[ \t]+" "" symbol "${row}")
|
|
37
|
+
string(APPEND definition "${symbol}\n")
|
|
38
|
+
endforeach()
|
|
39
|
+
set(definition_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.def")
|
|
40
|
+
set(${name}_IMPORT_LIBRARY "${CMAKE_CURRENT_BINARY_DIR}/${name}.lib")
|
|
41
|
+
file(WRITE "${definition_path}" "${definition}")
|
|
42
|
+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "[Aa][Rr][Mm]64|aarch64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64")
|
|
43
|
+
set(import_machine ARM64)
|
|
44
|
+
elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
|
45
|
+
set(import_machine X64)
|
|
46
|
+
else()
|
|
47
|
+
set(import_machine X86)
|
|
48
|
+
endif()
|
|
49
|
+
execute_process(COMMAND "${LIB_EXECUTABLE}" /NOLOGO "/DEF:${definition_path}"
|
|
50
|
+
"/OUT:${${name}_IMPORT_LIBRARY}" "/MACHINE:${import_machine}"
|
|
51
|
+
OUTPUT_VARIABLE import_output ERROR_VARIABLE import_error RESULT_VARIABLE import_result)
|
|
52
|
+
if(NOT import_result EQUAL 0)
|
|
53
|
+
message(FATAL_ERROR "${import_output}${import_error}")
|
|
54
|
+
endif()
|
|
55
|
+
endif()
|
|
56
|
+
add_library(upstream_${name} SHARED IMPORTED)
|
|
57
|
+
set_target_properties(upstream_${name} PROPERTIES
|
|
58
|
+
IMPORTED_LOCATION "${${name}_DLL}"
|
|
59
|
+
IMPORTED_IMPLIB "${${name}_IMPORT_LIBRARY}")
|
|
60
|
+
else()
|
|
61
|
+
find_library(${name}_LIBRARY NAMES "${name}"
|
|
62
|
+
PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/lib"
|
|
63
|
+
REQUIRED NO_DEFAULT_PATH)
|
|
64
|
+
add_library(upstream_${name} SHARED IMPORTED)
|
|
65
|
+
set_target_properties(upstream_${name} PROPERTIES IMPORTED_LOCATION "${${name}_LIBRARY}")
|
|
66
|
+
endif()
|
|
67
|
+
endfunction()
|
|
68
|
+
|
|
69
|
+
import_whisper_library(whisper)
|
|
70
|
+
import_whisper_library(ggml)
|
|
71
|
+
find_package(Threads REQUIRED)
|
|
72
|
+
|
|
73
|
+
add_executable(arcane-whisper main.cpp)
|
|
74
|
+
target_compile_features(arcane-whisper PRIVATE cxx_std_17)
|
|
75
|
+
target_compile_definitions(arcane-whisper PRIVATE WHISPER_SHARED GGML_SHARED)
|
|
76
|
+
target_include_directories(arcane-whisper PRIVATE "${WHISPER_INCLUDE_DIRECTORY}" "${GGML_INCLUDE_DIRECTORY}")
|
|
77
|
+
target_link_libraries(arcane-whisper PRIVATE upstream_whisper upstream_ggml Threads::Threads)
|
|
78
|
+
if(MSVC)
|
|
79
|
+
target_compile_options(arcane-whisper PRIVATE /utf-8 /EHsc)
|
|
80
|
+
endif()
|
|
81
|
+
if(APPLE)
|
|
82
|
+
set_target_properties(arcane-whisper PROPERTIES INSTALL_RPATH "@loader_path;@loader_path/../lib")
|
|
83
|
+
elseif(UNIX)
|
|
84
|
+
set_target_properties(arcane-whisper PROPERTIES INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib")
|
|
85
|
+
endif()
|
|
86
|
+
install(TARGETS arcane-whisper RUNTIME DESTINATION bin)
|
|
87
|
+
|
|
88
|
+
if(WIN32 AND MSVC)
|
|
89
|
+
# The published Whisper DLLs and this helper share the release MSVC CRT.
|
|
90
|
+
# Keep its permitted app-local redistributables beside the executable.
|
|
91
|
+
set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION bin)
|
|
92
|
+
set(CMAKE_INSTALL_DEBUG_LIBRARIES FALSE)
|
|
93
|
+
set(CMAKE_INSTALL_DEBUG_LIBRARIES_ONLY FALSE)
|
|
94
|
+
set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE)
|
|
95
|
+
set(CMAKE_INSTALL_MFC_LIBRARIES FALSE)
|
|
96
|
+
set(CMAKE_INSTALL_OPENMP_LIBRARIES TRUE)
|
|
97
|
+
include(InstallRequiredSystemLibraries)
|
|
98
|
+
endif()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Arcane Whisper native helper
|
|
2
|
+
Copyright (c) The Wizard Nexus and contributors.
|
|
3
|
+
|
|
4
|
+
The first-party helper source is licensed under AGPL-3.0-only; see LICENSE.
|
|
5
|
+
Matching source and build instructions are included in source/ and in the
|
|
6
|
+
corresponding Arcane SDK numeric release.
|
|
7
|
+
|
|
8
|
+
This Windows helper distribution includes unmodified Microsoft Visual C++
|
|
9
|
+
release runtime libraries from the installed Visual Studio 2022 Redistributable
|
|
10
|
+
tree. Those files retain Microsoft's terms. Their published distribution list
|
|
11
|
+
and application-local deployment documentation are available at:
|
|
12
|
+
https://learn.microsoft.com/en-us/visualstudio/releases/2022/redistribution
|
|
13
|
+
https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files
|
|
14
|
+
|
|
15
|
+
The helper uses the public whisper.cpp and GGML interfaces. Their MIT license
|
|
16
|
+
is included as licenses/whisper-LICENSE. The upstream runtime archives, model
|
|
17
|
+
and FFmpeg decoder are acquired separately through their selected upstream
|
|
18
|
+
distribution routes; they are not included in this helper archive.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Persistent Whisper helper
|
|
2
|
+
|
|
3
|
+
This first-party executable links the public C API from the selected upstream
|
|
4
|
+
whisper.cpp runtime. The current integration selects whisper.cpp 1.9.4/b5130.
|
|
5
|
+
The matching upstream headers and complete runtime remain in their installed
|
|
6
|
+
locations; they are not copied into SDK source. CMake builds only this helper.
|
|
7
|
+
|
|
8
|
+
`buildWhisperHelper` in `../build.mjs` takes the runtime's `directory`, matching
|
|
9
|
+
upstream `sourceDirectory`, and a task-owned `outputRoot`. Optional `cmake`,
|
|
10
|
+
`generator`, `cmakeArgs`, and `env` select an already available host toolchain.
|
|
11
|
+
Its `signal` and `onEvent` use the SDK process owner. The result names the
|
|
12
|
+
helper executable and retains complete configure, build, and installation
|
|
13
|
+
diagnostics. On Windows, when the upstream distribution omits import libraries,
|
|
14
|
+
the selected MSVC `dumpbin` and `lib` generate ordinary import libraries from
|
|
15
|
+
the actual DLL exports. This does not rebuild Whisper, ggml, or CUDA.
|
|
16
|
+
The Windows MSVC installation also uses CMake's
|
|
17
|
+
`InstallRequiredSystemLibraries` module to place the selected release CRT and
|
|
18
|
+
OpenMP redistributables beside the helper in `bin/`. Debug runtimes, MFC, and
|
|
19
|
+
Windows Universal CRT files are excluded; Windows 10/11 supplies the latter.
|
|
20
|
+
An explicit `-DMSVC_REDIST_DIR=...` in `cmakeArgs` can select the permitted
|
|
21
|
+
installed compiler redistributable tree for that build.
|
|
22
|
+
|
|
23
|
+
Windows uses the selected runtime directory in the child process's `PATH`.
|
|
24
|
+
Linux and macOS use their normal library loader configuration for that runtime.
|
|
25
|
+
The helper source uses standard C++17, with Windows binary standard-stream
|
|
26
|
+
mode as its only host-specific source adaptation. Android needs its native host
|
|
27
|
+
to provide the executable, upstream runtime, decoder, and process integration.
|
|
28
|
+
Availability of this source does not establish execution on another platform.
|
|
29
|
+
|
|
30
|
+
## Launch and commands
|
|
31
|
+
|
|
32
|
+
Launch with these arguments:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
arcane-whisper --model-base64 MODEL_PATH --runtime-base64 RUNTIME_PATH --backend gpu --threads 4
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Both path arguments are the base64 encoding of the complete UTF-8 path. The
|
|
39
|
+
backend is `gpu` or `cpu`. The optional thread argument selects native inference
|
|
40
|
+
threads; its default is the host's reported hardware concurrency, or one when
|
|
41
|
+
the host does not report it. The parent keeps runtime library resolution and
|
|
42
|
+
the backend directory aligned with the same selected runtime tree.
|
|
43
|
+
|
|
44
|
+
Each stdin command ends with one LF. The displayed `TAB` means an actual tab:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
transcribe TAB REQUEST_ID TAB PCM_PATH_BASE64 TAB LANGUAGE_BASE64 TAB TRANSLATE
|
|
48
|
+
cancel TAB REQUEST_ID
|
|
49
|
+
shutdown
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Request identifiers are parent-generated control identifiers. Paths and
|
|
53
|
+
languages use lossless base64 framing; `TRANSLATE` is `0` or `1`. The language
|
|
54
|
+
is a Whisper language name/code, `auto`, or empty for detection. The PCM file is
|
|
55
|
+
the complete decoded recording in mono 16 kHz f32le format. The decoder owns
|
|
56
|
+
that required representation; the helper adds no trimming, voice-activity
|
|
57
|
+
filter, duration restriction, offset, or audio normalization. The original
|
|
58
|
+
recording remains with the request owner. The native `whisper_full` signed-int
|
|
59
|
+
sample argument is an upstream representational limit; a recording it cannot
|
|
60
|
+
represent produces an explicit error without silently shortening the input.
|
|
61
|
+
|
|
62
|
+
## Events and ownership
|
|
63
|
+
|
|
64
|
+
Stdout contains complete JSON records, one per LF, each explicitly flushed.
|
|
65
|
+
Native diagnostics remain complete on stderr.
|
|
66
|
+
|
|
67
|
+
| `type` | Fields and meaning |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `loading` | `model`, `requestedBackend`; the retained context is being initialized. |
|
|
70
|
+
| `ready` | `model`, `requestedBackend`, `observedBackend`, `backendEvidence`; model initialization and state allocation returned successfully. |
|
|
71
|
+
| `accepted` | `requestId`; this request owns the retained context. |
|
|
72
|
+
| `progress` | `requestId`, `progress`; the actual upstream percentage callback, not an elapsed-time prediction. |
|
|
73
|
+
| `segment` | `requestId`, `index`, `text`, `start`, `end`; an actual newly produced native segment, in original callback order. |
|
|
74
|
+
| `complete` | `requestId`, `language`, `duration`, `text`, `segments`; the full native result after the inference worker joined. |
|
|
75
|
+
| `cancelled` | `requestId`; the cancellation was observed and the actual inference worker joined. |
|
|
76
|
+
| `error` | `requestId`, `message`; an actual operation failure. Startup failures use an empty request identifier. `code: "WHISPER_INFERENCE_FAILED"` identifies only a nonzero return from `whisper_full` after cancellation has been excluded. |
|
|
77
|
+
| `stopped` | The command reader joined and the retained context was freed. Process exit remains the parent's lifetime boundary. |
|
|
78
|
+
|
|
79
|
+
Segment start/end and recording duration are in seconds. Text preserves native
|
|
80
|
+
segment whitespace. Complete text concatenates the native segments in order,
|
|
81
|
+
with no trimming, inserted separators, or rewritten model output. No segment is
|
|
82
|
+
fabricated from tokens or inferred progress. The helper uses upstream greedy
|
|
83
|
+
decoding defaults with independent request context, zero offset, and the full
|
|
84
|
+
decoded duration.
|
|
85
|
+
|
|
86
|
+
The public C API has no context-backend getter. `observedBackend` therefore
|
|
87
|
+
reports the backend identified by completed native initialization logs, with
|
|
88
|
+
`backendEvidence: "runtime-log"`; it is null if those logs provided no usable
|
|
89
|
+
observation. It does not promise that every operation runs on a GPU. GPU
|
|
90
|
+
failure and CPU runtime selection belong to the parent runtime owner.
|
|
91
|
+
|
|
92
|
+
One active inference uses the retained context. A separate command reader can
|
|
93
|
+
set its cancellation flag while computation runs. Both the native encoder and
|
|
94
|
+
compute abort callbacks observe that flag. Native work drains before the main
|
|
95
|
+
dispatcher joins the inference worker and emits a terminal event. Upstream may
|
|
96
|
+
finish an already running GPU operation before observing cancellation.
|
|
97
|
+
Callbacks produced before native cancellation finishes remain complete events;
|
|
98
|
+
the parent owns whether a cancelled operation may affect its visible result.
|
|
99
|
+
|
|
100
|
+
`shutdown` and stdin EOF cancel active inference, join it and the command
|
|
101
|
+
reader, and free the model. Model loading has no public upstream cancellation
|
|
102
|
+
callback, so the parent cancels loading by terminating and joining its exact
|
|
103
|
+
helper process. A different selected model likewise belongs to a new retained
|
|
104
|
+
helper lifetime after the previous owner drains.
|