lol

Merge pull request #307020 from ck3d/local-ai-2130

local-ai: 2.12.4 -> 2.13.0

authored by

Christian Kögler and committed by
GitHub
56cfc87a adc9566b

+298 -116
+30
pkgs/by-name/lo/local-ai/lib.nix
··· 1 + { lib 2 + , writers 3 + , writeText 4 + , linkFarmFromDrvs 5 + }: { 6 + genModels = configs: 7 + let 8 + name = lib.strings.sanitizeDerivationName 9 + (builtins.concatStringsSep "_" ([ "local-ai-models" ] ++ (builtins.attrNames configs))); 10 + 11 + genModelFiles = name: config: 12 + let 13 + templateName = type: name + "_" + type; 14 + 15 + config' = lib.recursiveUpdate config ({ 16 + inherit name; 17 + } // lib.optionalAttrs (lib.isDerivation config.parameters.model) { 18 + parameters.model = config.parameters.model.name; 19 + } // lib.optionalAttrs (config ? template) { 20 + template = builtins.mapAttrs (n: _: templateName n) config.template; 21 + }); 22 + in 23 + [ (writers.writeYAML "${name}.yaml" config') ] 24 + ++ lib.optional (lib.isDerivation config.parameters.model) 25 + config.parameters.model 26 + ++ lib.optionals (config ? template) 27 + (lib.mapAttrsToList (n: writeText "${templateName n}.tmpl") config.template); 28 + in 29 + linkFarmFromDrvs name (lib.flatten (lib.mapAttrsToList genModelFiles configs)); 30 + }
+56
pkgs/by-name/lo/local-ai/module.nix
··· 1 + { pkgs, config, lib, ... }: 2 + let 3 + cfg = config.services.local-ai; 4 + inherit (lib) mkOption types; 5 + in 6 + { 7 + options.services.local-ai = { 8 + enable = lib.mkEnableOption "Enable service"; 9 + 10 + package = lib.mkPackageOption pkgs "local-ai" { }; 11 + 12 + extraArgs = mkOption { 13 + type = types.listOf types.str; 14 + default = [ ]; 15 + }; 16 + 17 + port = mkOption { 18 + type = types.port; 19 + default = 8080; 20 + }; 21 + 22 + threads = mkOption { 23 + type = types.int; 24 + default = 1; 25 + }; 26 + 27 + models = mkOption { 28 + type = types.either types.package types.str; 29 + default = "models"; 30 + }; 31 + }; 32 + 33 + config = lib.mkIf cfg.enable { 34 + systemd.services.local-ai = { 35 + wantedBy = [ "multi-user.target" ]; 36 + serviceConfig = { 37 + DynamicUser = true; 38 + ExecStart = lib.escapeShellArgs ([ 39 + "${cfg.package}/bin/local-ai" 40 + "--debug" 41 + "--address" 42 + ":${toString cfg.port}" 43 + "--threads" 44 + (toString cfg.threads) 45 + "--localai-config-dir" 46 + "." 47 + "--models-path" 48 + (toString cfg.models) 49 + ] 50 + ++ cfg.extraArgs); 51 + RuntimeDirectory = "local-ai"; 52 + WorkingDirectory = "%t/local-ai"; 53 + }; 54 + }; 55 + }; 56 + }
+24 -16
pkgs/by-name/lo/local-ai/package.nix
··· 6 6 , fetchpatch 7 7 , fetchFromGitHub 8 8 , protobuf 9 + , protoc-gen-go 10 + , protoc-gen-go-grpc 9 11 , grpc 10 12 , openssl 11 13 , llama-cpp ··· 61 63 62 64 inherit (cudaPackages) libcublas cuda_nvcc cuda_cccl cuda_cudart cudatoolkit; 63 65 64 - go-llama-ggml = effectiveStdenv.mkDerivation { 65 - name = "go-llama-ggml"; 66 + go-llama = effectiveStdenv.mkDerivation { 67 + name = "go-llama"; 66 68 src = fetchFromGitHub { 67 69 owner = "go-skynet"; 68 70 repo = "go-llama.cpp"; ··· 98 100 src = fetchFromGitHub { 99 101 owner = "ggerganov"; 100 102 repo = "llama.cpp"; 101 - rev = "1b67731e184e27a465b8c5476061294a4af668ea"; 102 - hash = "sha256-0WWbsklpW6HhFRkvWpYh8Lhi8VIansS/zmyIKNQRkIs="; 103 + rev = "784e11dea1f5ce9638851b2b0dddb107e2a609c8"; 104 + hash = "sha256-yAQAUo5J+a6O2kTqhFL1UH0tANxpQn3JhAd3MByaC6I="; 103 105 fetchSubmodules = true; 104 106 }; 105 107 postPatch = prev.postPatch + '' ··· 252 254 src = fetchFromGitHub { 253 255 owner = "ggerganov"; 254 256 repo = "whisper.cpp"; 255 - rev = "8f253ef3af1c62c04316ba4afa7145fc4d701a8c"; 256 - hash = "sha256-yHHjhpQIn99A/hqFwAb7TfTf4Q9KnKat93zyXS70bT8="; 257 + rev = "858452d58dba3acdc3431c9bced2bb8cfd9bf418"; 258 + hash = "sha256-2fT3RgGpBex1mF6GJsVDo4rb0F31YqxTymsXcrpQAZk="; 257 259 }; 258 260 259 261 nativeBuildInputs = [ cmake pkg-config ] ··· 371 373 stdenv; 372 374 373 375 pname = "local-ai"; 374 - version = "2.12.4"; 376 + version = "2.13.0"; 375 377 src = fetchFromGitHub { 376 378 owner = "go-skynet"; 377 379 repo = "LocalAI"; 378 380 rev = "v${version}"; 379 - hash = "sha256-piu2B6u4ZfxiOd9SXrE7jiiiwL2SM8EqXo2s5qeKRl0="; 381 + hash = "sha256-jZE8Ow9FFhnx/jvsURLYlYtSuKpE4UWBezxg/mpHs9g="; 380 382 }; 381 383 382 384 self = buildGoModule.override { stdenv = effectiveStdenv; } { 383 385 inherit pname version src; 384 386 385 - vendorHash = "sha256-8Hu1y/PK21twnB7D22ltslFFzRrsB8d1R2hkgIFB/XY="; 387 + vendorHash = "sha256-nWNK2YekQnBSLx4ouNSe6esIe0yFuo69E0HStYLQANg="; 386 388 387 389 env.NIX_CFLAGS_COMPILE = lib.optionalString with_stablediffusion " -isystem ${opencv}/include/opencv4"; 388 390 ··· 392 394 in 393 395 '' 394 396 sed -i Makefile \ 395 - -e 's;git clone.*go-llama-ggml$;${cp} ${go-llama-ggml} sources/go-llama-ggml;' \ 397 + -e 's;git clone.*go-llama\.cpp$;${cp} ${go-llama} sources/go-llama\.cpp;' \ 396 398 -e 's;git clone.*gpt4all$;${cp} ${gpt4all} sources/gpt4all;' \ 397 399 -e 's;git clone.*go-piper$;${cp} ${if with_tts then go-piper else go-piper.src} sources/go-piper;' \ 398 - -e 's;git clone.*go-rwkv$;${cp} ${go-rwkv} sources/go-rwkv;' \ 400 + -e 's;git clone.*go-rwkv\.cpp$;${cp} ${go-rwkv} sources/go-rwkv\.cpp;' \ 399 401 -e 's;git clone.*whisper\.cpp$;${cp} ${whisper-cpp.src} sources/whisper\.cpp;' \ 400 - -e 's;git clone.*go-bert$;${cp} ${go-bert} sources/go-bert;' \ 402 + -e 's;git clone.*go-bert\.cpp$;${cp} ${go-bert} sources/go-bert\.cpp;' \ 401 403 -e 's;git clone.*diffusion$;${cp} ${if with_stablediffusion then go-stable-diffusion else go-stable-diffusion.src} sources/go-stable-diffusion;' \ 402 404 -e 's;git clone.*go-tiny-dream$;${cp} ${if with_tinydream then go-tiny-dream else go-tiny-dream.src} sources/go-tiny-dream;' \ 403 405 -e 's, && git checkout.*,,g' \ ··· 415 417 ++ lib.optionals with_stablediffusion go-stable-diffusion.buildInputs 416 418 ++ lib.optionals with_tts go-piper.buildInputs; 417 419 418 - nativeBuildInputs = [ makeWrapper ] 419 - ++ lib.optionals with_cublas [ cuda_nvcc ]; 420 + nativeBuildInputs = [ 421 + protobuf 422 + protoc-gen-go 423 + protoc-gen-go-grpc 424 + makeWrapper 425 + ] 426 + ++ lib.optionals with_cublas [ cuda_nvcc ]; 420 427 421 428 enableParallelBuilding = false; 422 429 423 430 modBuildPhase = '' 424 431 mkdir sources 425 - make prepare-sources 432 + make prepare-sources protogen-go 426 433 go mod tidy -v 427 434 ''; 428 435 ··· 486 493 487 494 passthru.local-packages = { 488 495 inherit 489 - go-tiny-dream go-rwkv go-bert go-llama-ggml gpt4all go-piper 496 + go-tiny-dream go-rwkv go-bert go-llama gpt4all go-piper 490 497 llama-cpp-grpc whisper-cpp go-tiny-dream-ncnn espeak-ng' piper-phonemize 491 498 piper-tts'; 492 499 }; ··· 498 505 }; 499 506 500 507 passthru.tests = callPackages ./tests.nix { inherit self; }; 508 + passthru.lib = callPackages ./lib.nix { }; 501 509 502 510 meta = with lib; { 503 511 description = "OpenAI alternative to run local LLMs, image and audio generation";
+188 -100
pkgs/by-name/lo/local-ai/tests.nix
··· 5 5 , fetchurl 6 6 , writers 7 7 , symlinkJoin 8 - , linkFarmFromDrvs 9 8 , jq 10 9 }: 10 + let 11 + common-config = { config, ... }: { 12 + imports = [ ./module.nix ]; 13 + services.local-ai = { 14 + enable = true; 15 + package = self; 16 + threads = config.virtualisation.cores; 17 + }; 18 + }; 19 + 20 + inherit (self.lib) genModels; 21 + in 11 22 { 12 23 version = testers.testVersion { 13 24 package = self; 14 25 version = "v" + self.version; 26 + command = "local-ai --help"; 15 27 }; 16 28 17 - health = 29 + health = testers.runNixOSTest ({ config, ... }: { 30 + name = self.name + "-health"; 31 + nodes.machine = common-config; 32 + testScript = 33 + let 34 + port = "8080"; 35 + in 36 + '' 37 + machine.wait_for_open_port(${port}) 38 + machine.succeed("curl -f http://localhost:${port}/readyz") 39 + ''; 40 + }); 41 + 42 + # https://localai.io/features/embeddings/#bert-embeddings 43 + bert = 18 44 let 19 - port = "8080"; 45 + model = "embedding"; 46 + model-configs.${model} = { 47 + # Note: q4_0 and q4_1 models can not be loaded 48 + parameters.model = fetchurl { 49 + url = "https://huggingface.co/skeskinen/ggml/resolve/main/all-MiniLM-L6-v2/ggml-model-f16.bin"; 50 + sha256 = "9c195b2453a4fef60a4f6be3a88a39211366214df6498a4fe4885c9e22314f50"; 51 + }; 52 + backend = "bert-embeddings"; 53 + embeddings = true; 54 + }; 55 + 56 + models = genModels model-configs; 57 + 58 + requests.request = { 59 + inherit model; 60 + input = "Your text string goes here"; 61 + }; 20 62 in 21 63 testers.runNixOSTest { 22 - name = self.name + "-health"; 64 + name = self.name + "-bert"; 23 65 nodes.machine = { 24 - systemd.services.local-ai = { 25 - wantedBy = [ "multi-user.target" ]; 26 - serviceConfig.ExecStart = "${self}/bin/local-ai --debug --localai-config-dir . --address :${port}"; 27 - }; 66 + imports = [ common-config ]; 67 + virtualisation.cores = 2; 68 + virtualisation.memorySize = 2048; 69 + services.local-ai.models = models; 28 70 }; 29 - testScript = '' 30 - machine.wait_for_open_port(${port}) 31 - machine.succeed("curl -f http://localhost:${port}/readyz") 32 - ''; 71 + passthru = { inherit models requests; }; 72 + testScript = 73 + let 74 + port = "8080"; 75 + in 76 + '' 77 + machine.wait_for_open_port(${port}) 78 + machine.succeed("curl -f http://localhost:${port}/readyz") 79 + machine.succeed("curl -f http://localhost:${port}/v1/models --output models.json") 80 + machine.succeed("${jq}/bin/jq --exit-status 'debug | .data[].id == \"${model}\"' models.json") 81 + machine.succeed("curl -f http://localhost:${port}/embeddings --json @${writers.writeJSON "request.json" requests.request} --output embeddings.json") 82 + machine.succeed("${jq}/bin/jq --exit-status 'debug | .model == \"${model}\"' embeddings.json") 83 + ''; 33 84 }; 34 85 86 + } // lib.optionalAttrs (!self.features.with_cublas && !self.features.with_clblas) { 35 87 # https://localai.io/docs/getting-started/manual/ 36 88 llama = 37 89 let 38 - port = "8080"; 39 - gguf = fetchurl { 40 - url = "https://huggingface.co/TheBloke/Luna-AI-Llama2-Uncensored-GGUF/resolve/main/luna-ai-llama2-uncensored.Q4_K_M.gguf"; 41 - sha256 = "6a9dc401c84f0d48996eaa405174999c3a33bf12c2bfd8ea4a1e98f376de1f15"; 90 + model = "gpt-3.5-turbo"; 91 + 92 + # https://localai.io/advanced/#full-config-model-file-reference 93 + model-configs.${model} = rec { 94 + context_size = 8192; 95 + parameters = { 96 + # https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF 97 + # https://ai.meta.com/blog/meta-llama-3/ 98 + model = fetchurl { 99 + url = "https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf"; 100 + sha256 = "ab9e4eec7e80892fd78f74d9a15d0299f1e22121cea44efd68a7a02a3fe9a1da"; 101 + }; 102 + # defaults from: 103 + # https://deepinfra.com/meta-llama/Meta-Llama-3-8B-Instruct 104 + temperature = 0.7; 105 + top_p = 0.9; 106 + top_k = 0; 107 + # following parameter leads to outputs like: !!!!!!!!!!!!!!!!!!! 108 + #repeat_penalty = 1; 109 + presence_penalty = 0; 110 + frequency_penalty = 0; 111 + max_tokens = 100; 112 + }; 113 + stopwords = [ "<|eot_id|>" ]; 114 + template = { 115 + # Templates implement following specifications 116 + # https://github.com/meta-llama/llama3/tree/main?tab=readme-ov-file#instruction-tuned-models 117 + # ... and are insprired by: 118 + # https://github.com/mudler/LocalAI/blob/master/embedded/models/llama3-instruct.yaml 119 + # 120 + # The rules for template evaluateion are defined here: 121 + # https://pkg.go.dev/text/template 122 + chat_message = '' 123 + <|start_header_id|>{{.RoleName}}<|end_header_id|> 124 + 125 + {{.Content}}${builtins.head stopwords}''; 126 + 127 + chat = "<|begin_of_text|>{{.Input}}<|start_header_id|>assistant<|end_header_id|>"; 128 + }; 42 129 }; 43 - models = linkFarmFromDrvs "models" [ 44 - gguf 45 - ]; 130 + 131 + models = genModels model-configs; 132 + 133 + requests = { 134 + # https://localai.io/features/text-generation/#chat-completions 135 + chat-completions = { 136 + inherit model; 137 + messages = [{ role = "user"; content = "1 + 2 = ?"; }]; 138 + }; 139 + # https://localai.io/features/text-generation/#edit-completions 140 + edit-completions = { 141 + inherit model; 142 + instruction = "rephrase"; 143 + input = "Black cat jumped out of the window"; 144 + max_tokens = 50; 145 + }; 146 + # https://localai.io/features/text-generation/#completions 147 + completions = { 148 + inherit model; 149 + prompt = "A long time ago in a galaxy far, far away"; 150 + }; 151 + }; 46 152 in 47 153 testers.runNixOSTest { 48 154 name = self.name + "-llama"; 49 - nodes.machine = 50 - let 51 - cores = 4; 52 - in 53 - { 54 - virtualisation = { 55 - inherit cores; 56 - memorySize = 8192; 57 - }; 58 - systemd.services.local-ai = { 59 - wantedBy = [ "multi-user.target" ]; 60 - serviceConfig.ExecStart = "${self}/bin/local-ai --debug --threads ${toString cores} --models-path ${models} --localai-config-dir . --address :${port}"; 61 - }; 62 - }; 155 + nodes.machine = { 156 + imports = [ common-config ]; 157 + virtualisation.cores = 4; 158 + virtualisation.memorySize = 8192; 159 + services.local-ai.models = models; 160 + }; 161 + passthru = { inherit models requests; }; 63 162 testScript = 64 163 let 65 - # https://localai.io/features/text-generation/#chat-completions 66 - request-chat-completions = { 67 - model = gguf.name; 68 - messages = [{ role = "user"; content = "Say this is a test!"; }]; 69 - temperature = 0.7; 70 - }; 71 - # https://localai.io/features/text-generation/#edit-completions 72 - request-edit-completions = { 73 - model = gguf.name; 74 - instruction = "rephrase"; 75 - input = "Black cat jumped out of the window"; 76 - temperature = 0.7; 77 - }; 78 - # https://localai.io/features/text-generation/#completions 79 - request-completions = { 80 - model = gguf.name; 81 - prompt = "A long time ago in a galaxy far, far away"; 82 - temperature = 0.7; 83 - }; 164 + port = "8080"; 84 165 in 85 166 '' 86 167 machine.wait_for_open_port(${port}) 87 168 machine.succeed("curl -f http://localhost:${port}/readyz") 88 169 machine.succeed("curl -f http://localhost:${port}/v1/models --output models.json") 89 - machine.succeed("${jq}/bin/jq --exit-status 'debug | .data[].id == \"${gguf.name}\"' models.json") 90 - machine.succeed("curl -f http://localhost:${port}/v1/chat/completions --json @${writers.writeJSON "request-chat-completions.json" request-chat-completions} --output chat-completions.json") 170 + machine.succeed("${jq}/bin/jq --exit-status 'debug | .data[].id == \"${model}\"' models.json") 171 + 172 + machine.succeed("curl -f http://localhost:${port}/v1/chat/completions --json @${writers.writeJSON "request-chat-completions.json" requests.chat-completions} --output chat-completions.json") 91 173 machine.succeed("${jq}/bin/jq --exit-status 'debug | .object == \"chat.completion\"' chat-completions.json") 92 - machine.succeed("curl -f http://localhost:${port}/v1/edits --json @${writers.writeJSON "request-edit-completions.json" request-edit-completions} --output edit-completions.json") 174 + machine.succeed("${jq}/bin/jq --exit-status 'debug | .choices | first.message.content | tonumber == 3' chat-completions.json") 175 + 176 + machine.succeed("curl -f http://localhost:${port}/v1/edits --json @${writers.writeJSON "request-edit-completions.json" requests.edit-completions} --output edit-completions.json") 93 177 machine.succeed("${jq}/bin/jq --exit-status 'debug | .object == \"edit\"' edit-completions.json") 94 - machine.succeed("curl -f http://localhost:${port}/v1/completions --json @${writers.writeJSON "request-completions.json" request-completions} --output completions.json") 178 + machine.succeed("${jq}/bin/jq --exit-status '.usage.completion_tokens | debug == ${toString requests.edit-completions.max_tokens}' edit-completions.json") 179 + 180 + machine.succeed("curl -f http://localhost:${port}/v1/completions --json @${writers.writeJSON "request-completions.json" requests.completions} --output completions.json") 95 181 machine.succeed("${jq}/bin/jq --exit-status 'debug | .object ==\"text_completion\"' completions.json") 182 + machine.succeed("${jq}/bin/jq --exit-status '.usage.completion_tokens | debug == ${toString model-configs.${model}.parameters.max_tokens}' completions.json") 96 183 ''; 97 184 }; 98 185 99 - } // lib.optionalAttrs self.features.with_tts { 186 + } // lib.optionalAttrs (self.features.with_tts && !self.features.with_cublas && !self.features.with_clblas) { 100 187 # https://localai.io/features/text-to-audio/#piper 101 188 tts = 102 189 let 103 - port = "8080"; 104 - voice-en-us = fetchzip { 105 - url = "https://github.com/rhasspy/piper/releases/download/v0.0.2/voice-en-us-danny-low.tar.gz"; 106 - hash = "sha256-5wf+6H5HeQY0qgdqnAG1vSqtjIFM9lXH53OgouuPm0M="; 107 - stripRoot = false; 108 - }; 109 - ggml-tiny-en = fetchurl { 110 - url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q5_1.bin"; 111 - hash = "sha256-x3xXZvHO8JtrfUfyG1Rsvd1BV4hrO11tT3CekeZsfCs="; 112 - }; 113 - whisper-en = { 114 - name = "whisper-en"; 190 + model-stt = "whisper-en"; 191 + model-configs.${model-stt} = { 115 192 backend = "whisper"; 116 - parameters.model = ggml-tiny-en.name; 193 + parameters.model = fetchurl { 194 + url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q5_1.bin"; 195 + hash = "sha256-x3xXZvHO8JtrfUfyG1Rsvd1BV4hrO11tT3CekeZsfCs="; 196 + }; 117 197 }; 118 - models = symlinkJoin { 119 - name = "models"; 120 - paths = [ 121 - voice-en-us 122 - (linkFarmFromDrvs "whisper-en" [ 123 - (writers.writeYAML "whisper-en.yaml" whisper-en) 124 - ggml-tiny-en 125 - ]) 126 - ]; 198 + 199 + model-tts = "piper-en"; 200 + model-configs.${model-tts} = { 201 + backend = "piper"; 202 + parameters.model = "en-us-danny-low.onnx"; 127 203 }; 128 - in 129 - testers.runNixOSTest { 130 - name = self.name + "-tts"; 131 - nodes.machine = 204 + 205 + models = 132 206 let 133 - cores = 2; 207 + models = genModels model-configs; 134 208 in 135 - { 136 - virtualisation = { 137 - inherit cores; 138 - }; 139 - systemd.services.local-ai = { 140 - wantedBy = [ "multi-user.target" ]; 141 - serviceConfig.ExecStart = "${self}/bin/local-ai --debug --threads ${toString cores} --models-path ${models} --localai-config-dir . --address :${port}"; 142 - }; 209 + symlinkJoin { 210 + inherit (models) name; 211 + paths = [ 212 + models 213 + (fetchzip { 214 + url = "https://github.com/rhasspy/piper/releases/download/v0.0.2/voice-en-us-danny-low.tar.gz"; 215 + hash = "sha256-5wf+6H5HeQY0qgdqnAG1vSqtjIFM9lXH53OgouuPm0M="; 216 + stripRoot = false; 217 + }) 218 + ]; 143 219 }; 220 + 221 + requests.request = { 222 + model = model-tts; 223 + input = "Hello, how are you?"; 224 + }; 225 + in 226 + testers.runNixOSTest { 227 + name = self.name + "-tts"; 228 + nodes.machine = { 229 + imports = [ common-config ]; 230 + virtualisation.cores = 2; 231 + services.local-ai.models = models; 232 + }; 233 + passthru = { inherit models requests; }; 144 234 testScript = 145 235 let 146 - request = { 147 - model = "en-us-danny-low.onnx"; 148 - backend = "piper"; 149 - input = "Hello, how are you?"; 150 - }; 236 + port = "8080"; 151 237 in 152 238 '' 153 239 machine.wait_for_open_port(${port}) 154 240 machine.succeed("curl -f http://localhost:${port}/readyz") 155 - machine.succeed("curl -f http://localhost:${port}/tts --json @${writers.writeJSON "request.json" request} --output out.wav") 156 - machine.succeed("curl -f http://localhost:${port}/v1/audio/transcriptions --header 'Content-Type: multipart/form-data' --form file=@out.wav --form model=${whisper-en.name} --output transcription.json") 157 - machine.succeed("${jq}/bin/jq --exit-status 'debug | .segments | first.text == \"${request.input}\"' transcription.json") 241 + machine.succeed("curl -f http://localhost:${port}/v1/models --output models.json") 242 + machine.succeed("${jq}/bin/jq --exit-status 'debug' models.json") 243 + machine.succeed("curl -f http://localhost:${port}/tts --json @${writers.writeJSON "request.json" requests.request} --output out.wav") 244 + machine.succeed("curl -f http://localhost:${port}/v1/audio/transcriptions --header 'Content-Type: multipart/form-data' --form file=@out.wav --form model=${model-stt} --output transcription.json") 245 + machine.succeed("${jq}/bin/jq --exit-status 'debug | .segments | first.text == \"${requests.request.input}\"' transcription.json") 158 246 ''; 159 247 }; 160 248 }