const std = @import("std"); 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(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); exe.linkLibC(); 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"); const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { 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; } };