From 15df51e243f1e044af9b1de3e63c06cfa94a2254 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 9 Sep 2025 13:30:20 -0700 Subject: [PATCH] clean up a bit, move to dependencies, remove unnecessary nix packages --- build.zig | 192 +++++++++++++++++++++++++++++++++++++++++++++++++- build.zig.zon | 76 ++------------------ flake.nix | 3 - src/main.zig | 4 +- 4 files changed, 198 insertions(+), 77 deletions(-) diff --git a/build.zig b/build.zig index 9ee6d0c..b80e636 100644 --- a/build.zig +++ b/build.zig @@ -4,6 +4,20 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const vosk_dep = b.dependency("vosk", .{}); + + // Create a proper build step for model download with caching + // We need to use curl for this as the domain doesn't work with zig TLS + const model_step = ModelDownloadStep.create(b); + + // Install the model to the output directory + const install_model = b.addInstallDirectory(.{ + .source_dir = b.path("vosk-model-small-en-us-0.15"), + .install_dir = .bin, + .install_subdir = "vosk-model-small-en-us-0.15", + }); + install_model.step.dependOn(&model_step.step); + const exe = b.addExecutable(.{ .name = "stt", .root_module = b.createModule(.{ @@ -14,11 +28,13 @@ pub fn build(b: *std.Build) void { }); exe.linkLibC(); - exe.addIncludePath(b.path("vosk-linux-x86_64-0.3.45")); - exe.addLibraryPath(b.path("vosk-linux-x86_64-0.3.45")); + exe.addIncludePath(vosk_dep.path("")); + exe.addLibraryPath(vosk_dep.path("")); exe.linkSystemLibrary("vosk"); exe.linkSystemLibrary("asound"); + b.getInstallStep().dependOn(&install_model.step); + b.installArtifact(exe); const run_step = b.step("run", "Run the app"); @@ -30,3 +46,175 @@ pub fn build(b: *std.Build) void { run_cmd.addArgs(args); } } + +const ModelDownloadStep = struct { + step: std.Build.Step, + builder: *std.Build, + + pub fn create(builder: *std.Build) *ModelDownloadStep { + const self = builder.allocator.create(ModelDownloadStep) catch @panic("OOM"); + self.* = .{ + .step = std.Build.Step.init(.{ + .id = .custom, + .name = "download-model", + .owner = builder, + .makeFn = make, + }), + .builder = builder, + }; + return self; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void { + _ = options; + const self: *ModelDownloadStep = @fieldParentPtr("step", step); + + const model_dir = "vosk-model-small-en-us-0.15"; + + // Create a cache hash based on the URL + var hasher = std.hash.Wyhash.init(0); + hasher.update("https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip"); + const cache_hash = hasher.final(); + + var cache_dir_buf: [std.fs.max_path_bytes]u8 = undefined; + const cache_dir = std.fmt.bufPrint(&cache_dir_buf, "{s}/o/{x}", .{ self.builder.cache_root.path.?, cache_hash }) catch @panic("path too long"); + + const cached_model_dir = std.fmt.allocPrint(self.builder.allocator, "{s}/{s}", .{ cache_dir, model_dir }) catch @panic("OOM"); + defer self.builder.allocator.free(cached_model_dir); + + // Check if already cached + if (std.fs.cwd().access(cached_model_dir, .{})) |_| { + // Copy from cached version using stdlib + std.fs.cwd().deleteTree(model_dir) catch {}; + std.fs.cwd().makePath(model_dir) catch {}; + var cached_dir = std.fs.cwd().openDir(cached_model_dir, .{ .iterate = true }) catch return error.CopyFailed; + defer cached_dir.close(); + var target_dir = std.fs.cwd().openDir(model_dir, .{}) catch return error.CopyFailed; + defer target_dir.close(); + + var iter = cached_dir.iterate(); + while (iter.next() catch return error.CopyFailed) |entry| { + switch (entry.kind) { + .file => { + cached_dir.copyFile(entry.name, target_dir, entry.name, .{}) catch return error.CopyFailed; + }, + .directory => { + target_dir.makeDir(entry.name) catch {}; + var sub_cached = cached_dir.openDir(entry.name, .{ .iterate = true }) catch return error.CopyFailed; + defer sub_cached.close(); + var sub_target = target_dir.openDir(entry.name, .{}) catch return error.CopyFailed; + defer sub_target.close(); + + var sub_iter = sub_cached.iterate(); + while (sub_iter.next() catch return error.CopyFailed) |sub_entry| { + if (sub_entry.kind == .file) { + sub_cached.copyFile(sub_entry.name, sub_target, sub_entry.name, .{}) catch return error.CopyFailed; + } + } + }, + else => {}, + } + } + step.result_cached = true; + return; + } else |_| {} + + // Not cached, need to download + std.fs.cwd().makePath(cache_dir) catch {}; + + const model_zip = std.fmt.allocPrint(self.builder.allocator, "{s}/model.zip", .{cache_dir}) catch @panic("OOM"); + defer self.builder.allocator.free(model_zip); + + // Download + const download_result = std.process.Child.run(.{ + .allocator = self.builder.allocator, + .argv = &.{ "curl", "-s", "-L", "-o", model_zip, "https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip" }, + }) catch return error.DownloadFailed; + if (download_result.term.Exited != 0) return error.DownloadFailed; + + // Extract to cache + const extract_result = std.process.Child.run(.{ + .allocator = self.builder.allocator, + .argv = &.{ "unzip", "-o", model_zip, "-d", cache_dir }, + }) catch return error.UnzipFailed; + if (extract_result.term.Exited != 0) return error.UnzipFailed; + + // Copy to working directory using stdlib + std.fs.cwd().deleteTree(model_dir) catch {}; + std.fs.cwd().makePath(model_dir) catch {}; + var cached_dir = std.fs.cwd().openDir(cached_model_dir, .{ .iterate = true }) catch return error.CopyFailed; + defer cached_dir.close(); + var target_dir = std.fs.cwd().openDir(model_dir, .{}) catch return error.CopyFailed; + defer target_dir.close(); + + var iter = cached_dir.iterate(); + while (iter.next() catch return error.CopyFailed) |entry| { + switch (entry.kind) { + .file => { + cached_dir.copyFile(entry.name, target_dir, entry.name, .{}) catch return error.CopyFailed; + }, + .directory => { + target_dir.makeDir(entry.name) catch {}; + var sub_cached = cached_dir.openDir(entry.name, .{ .iterate = true }) catch return error.CopyFailed; + defer sub_cached.close(); + var sub_target = target_dir.openDir(entry.name, .{}) catch return error.CopyFailed; + defer sub_target.close(); + + var sub_iter = sub_cached.iterate(); + while (sub_iter.next() catch return error.CopyFailed) |sub_entry| { + if (sub_entry.kind == .file) { + sub_cached.copyFile(sub_entry.name, sub_target, sub_entry.name, .{}) catch return error.CopyFailed; + } + } + }, + else => {}, + } + } + + step.result_cached = false; + } +}; + +const ModelInstallStep = struct { + step: std.Build.Step, + builder: *std.Build, + model_step: *std.Build.Step, + + pub fn create(builder: *std.Build, model_step: *std.Build.Step) *ModelInstallStep { + const self = builder.allocator.create(ModelInstallStep) catch @panic("OOM"); + self.* = .{ + .step = std.Build.Step.init(.{ + .id = .custom, + .name = "install vosk-model-small-en-us-0.15/", + .owner = builder, + .makeFn = make, + }), + .builder = builder, + .model_step = model_step, + }; + self.step.dependOn(model_step); + return self; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void { + _ = options; + const self: *ModelInstallStep = @fieldParentPtr("step", step); + + const install_dir = self.builder.getInstallPath(.bin, "vosk-model-small-en-us-0.15"); + + // Inherit cache status from model step + step.result_cached = self.model_step.result_cached; + + if (step.result_cached) return; + + // Copy model to install directory + std.fs.cwd().makePath(std.fs.path.dirname(install_dir).?) catch {}; + std.fs.cwd().deleteTree(install_dir) catch {}; + + const copy_result = std.process.Child.run(.{ + .allocator = self.builder.allocator, + .argv = &.{ "cp", "-r", "vosk-model-small-en-us-0.15", install_dir }, + }) catch return error.CopyFailed; + if (copy_result.term.Exited != 0) return error.CopyFailed; + } +}; diff --git a/build.zig.zon b/build.zig.zon index f36eeea..de6f7f6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,81 +1,17 @@ .{ - // This is the default name used by packages depending on this one. For - // example, when a user runs `zig fetch --save `, this field is used - // as the key in the `dependencies` table. Although the user can choose a - // different name, most users will stick with this provided value. - // - // It is redundant to include "zig" in this name because it is already - // within the Zig package namespace. - .name = ._1_stt_2, - // This is a [Semantic Version](https://semver.org/). - // In a future version of Zig it will be used for package deduplication. + .name = .vosk_stt, .version = "0.0.0", - // Together with name, this represents a globally unique package - // identifier. This field is generated by the Zig toolchain when the - // package is first created, and then *never changes*. This allows - // unambiguous detection of one package being an updated version of - // another. - // - // When forking a Zig project, this id should be regenerated (delete the - // field and run `zig build`) if the upstream project is still maintained. - // Otherwise, the fork is *hostile*, attempting to take control over the - // original project's identity. Thus it is recommended to leave the comment - // on the following line intact, so that it shows up in code reviews that - // modify the field. - .fingerprint = 0xe6cb5784eea38627, // Changing this has security and trust implications. - // Tracks the earliest Zig version that the package considers to be a - // supported use case. + .fingerprint = 0xc855826ba95b9540, .minimum_zig_version = "0.15.1", - // This field is optional. - // Each dependency must either provide a `url` and `hash`, or a `path`. - // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. - // Once all dependencies are fetched, `zig build` no longer requires - // internet connectivity. .dependencies = .{ - // See `zig fetch --save ` for a command-line interface for adding dependencies. - //.example = .{ - // // When updating this field to a new URL, be sure to delete the corresponding - // // `hash`, otherwise you are communicating that you expect to find the old hash at - // // the new URL. If the contents of a URL change this will result in a hash mismatch - // // which will prevent zig from using it. - // .url = "https://example.com/foo.tar.gz", - // - // // This is computed from the file contents of the directory of files that is - // // obtained after fetching `url` and applying the inclusion rules given by - // // `paths`. - // // - // // This field is the source of truth; packages do not come from a `url`; they - // // come from a `hash`. `url` is just one of many possible mirrors for how to - // // obtain a package matching this `hash`. - // // - // // Uses the [multihash](https://multiformats.io/multihash/) format. - // .hash = "...", - // - // // When this is provided, the package is found in a directory relative to the - // // build root. In this case the package's hash is irrelevant and therefore not - // // computed. This field and `url` are mutually exclusive. - // .path = "foo", - // - // // When this is set to `true`, a package is declared to be lazily - // // fetched. This makes the dependency only get fetched if it is - // // actually used. - // .lazy = false, - //}, + .vosk = .{ + .url = "https://github.com/alphacep/vosk-api/releases/download/v0.3.45/vosk-linux-x86_64-0.3.45.zip", + .hash = "N-V-__8AAF22jAFTSU4AVxFCNWtotf7OD8gM33Y_ScIrCeu7", + }, }, - // Specifies the set of files and directories that are included in this package. - // Only files and directories listed here are included in the `hash` that - // is computed for this package. Only files listed here will remain on disk - // when using the zig package manager. As a rule of thumb, one should list - // files required for compilation plus any license(s). - // Paths are relative to the build root. Use the empty string (`""`) to refer to - // the build root itself. - // A directory listed here means that all files within, recursively, are included. .paths = .{ "build.zig", "build.zig.zon", "src", - // For example... - //"LICENSE", - //"README.md", }, } diff --git a/flake.nix b/flake.nix index 55103fa..4ba0282 100644 --- a/flake.nix +++ b/flake.nix @@ -14,9 +14,6 @@ { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ - clang - llvm - cmake pkg-config zlib alsa-lib diff --git a/src/main.zig b/src/main.zig index 2b8f132..99a4f59 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,7 +13,7 @@ pub fn main() !void { // Initialize Vosk c.vosk_set_log_level(-1); - const model = c.vosk_model_new("vosk-model-small-en-us-0.15"); + const model = c.vosk_model_new("zig-out/bin/vosk-model-small-en-us-0.15"); if (model == null) { std.debug.print("Failed to load model\n", .{}); return; @@ -56,7 +56,7 @@ pub fn main() !void { var buffer: [BUFFER_SIZE]i16 = undefined; var frame_count: u32 = 0; - + while (true) { const frames = c.snd_pcm_readi(handle, &buffer, BUFFER_SIZE / 2); if (frames < 0) {