Repository: promptfoo/promptfoo
Pull request #3618: chore(redteam): make audio strategy remote-only
Author: login: mldangelo
Created: 2025-04-08T06:29:27Z | Merged: 2025-04-11T00:53:39Z

resolves github security errors


Diff:
diff --git a/package-lock.json b/package-lock.json
index 7741227f3ef8..eddde9ba0587 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -151,7 +151,6 @@
         "google-auth-library": "^9.7.0",
         "ibm-cloud-sdk-core": "^5.0.2",
         "langfuse": "^3.7.0",
-        "node-gtts": "^2.0.2",
         "node-sql-parser": "^5.2.0",
         "pdf-parse": "^1.1.1",
         "playwright": "^1.47.2",
@@ -17055,15 +17054,6 @@
       "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
       "license": "MIT"
     },
-    "node_modules/asn1": {
-      "version": "0.2.6",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
-      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
-      "peer": true,
-      "dependencies": {
-        "safer-buffer": "~2.1.0"
-      }
-    },
     "node_modules/asn1.js": {
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@@ -17097,15 +17087,6 @@
         "util": "^0.12.5"
       }
     },
-    "node_modules/assert-plus": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
-      "peer": true,
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
     "node_modules/assertion-error": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -17247,21 +17228,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/aws-sign2": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-      "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
-      "peer": true,
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/aws4": {
-      "version": "1.13.2",
-      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
-      "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
-      "peer": true
-    },
     "node_modules/axios": {
       "version": "1.8.2",
       "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
@@ -17610,15 +17576,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/bcrypt-pbkdf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
-      "peer": true,
-      "dependencies": {
-        "tweetnacl": "^0.14.3"
-      }
-    },
     "node_modules/better-sqlite3": {
       "version": "11.9.1",
       "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz",
@@ -18375,12 +18332,6 @@
       "license": "MIT",
       "optional": true
     },
-    "node_modules/caseless": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
-      "peer": true
-    },
     "node_modules/ccount": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -19631,6 +19582,7 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/cors": {
@@ -20900,18 +20852,6 @@
         "lodash-es": "^4.17.21"
       }
     },
-    "node_modules/dashdash": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-      "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
-      "peer": true,
-      "dependencies": {
-        "assert-plus": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
     "node_modules/data-uri-to-buffer": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
@@ -22470,22 +22410,6 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "license": "MIT"
     },
-    "node_modules/ecc-jsbn": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-      "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
-      "peer": true,
-      "dependencies": {
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.1.0"
-      }
-    },
-    "node_modules/ecc-jsbn/node_modules/jsbn": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-      "peer": true
-    },
     "node_modules/ecdsa-sig-formatter": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -23767,21 +23691,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/extsprintf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
-      "engines": [
-        "node >=0.6.0"
-      ],
-      "peer": true
-    },
-    "node_modules/fake-useragent": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/fake-useragent/-/fake-useragent-1.0.1.tgz",
-      "integrity": "sha512-BOQh1TM//DhrVaeZ+b3w3s4E40rfYcDTn5aoSM2w1xVsZVGglNOzPR5H8KDO8NmF8sT4ppxyb4/MHGIHfZsVDA==",
-      "peer": true
-    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -23839,6 +23748,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/fast-levenshtein": {
@@ -24486,15 +24396,6 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/forever-agent": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-      "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
-      "peer": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/fork-ts-checker-webpack-plugin": {
       "version": "6.5.3",
       "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz",
@@ -25106,15 +25007,6 @@
         "node": ">= 14"
       }
     },
-    "node_modules/getpass": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-      "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
-      "peer": true,
-      "dependencies": {
-        "assert-plus": "^1.0.0"
-      }
-    },
     "node_modules/github-from-package": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -25483,51 +25375,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/har-schema": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-      "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
-      "peer": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/har-validator": {
-      "version": "5.1.5",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
-      "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
-      "deprecated": "this library is no longer supported",
-      "peer": true,
-      "dependencies": {
-        "ajv": "^6.12.3",
-        "har-schema": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/har-validator/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "peer": true,
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/har-validator/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "peer": true
-    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -26306,21 +26153,6 @@
         "node": ">= 14"
       }
     },
-    "node_modules/http-signature": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-      "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
-      "peer": true,
-      "dependencies": {
-        "assert-plus": "^1.0.0",
-        "jsprim": "^1.2.2",
-        "sshpk": "^1.7.0"
-      },
-      "engines": {
-        "node": ">=0.8",
-        "npm": ">=1.3.7"
-      }
-    },
     "node_modules/http-status-codes": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
@@ -26591,9 +26423,9 @@
       "license": "ISC"
     },
     "node_modules/image-size": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz",
-      "integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
+      "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -27368,6 +27200,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
       "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/is-unicode-supported": {
@@ -27430,6 +27263,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
       "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/isexe": {
@@ -28675,6 +28509,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
       "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/json5": {
@@ -28762,21 +28597,6 @@
         "safe-buffer": "^5.0.1"
       }
     },
-    "node_modules/jsprim": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
-      "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
-      "peer": true,
-      "dependencies": {
-        "assert-plus": "1.0.0",
-        "extsprintf": "1.3.0",
-        "json-schema": "0.4.0",
-        "verror": "1.10.0"
-      },
-      "engines": {
-        "node": ">=0.6.0"
-      }
-    },
     "node_modules/jwa": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
@@ -32418,46 +32238,6 @@
         "multicast-dns": "cli.js"
       }
     },
-    "node_modules/multistream": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz",
-      "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==",
-      "peer": true,
-      "dependencies": {
-        "inherits": "^2.0.1",
-        "readable-stream": "^2.0.5"
-      }
-    },
-    "node_modules/multistream/node_modules/readable-stream": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
-      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
-      "peer": true,
-      "dependencies": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "node_modules/multistream/node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-      "peer": true
-    },
-    "node_modules/multistream/node_modules/string_decoder": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "peer": true,
-      "dependencies": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
     "node_modules/mustache": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
@@ -32676,40 +32456,6 @@
         "node": ">= 6.13.0"
       }
     },
-    "node_modules/node-gtts": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/node-gtts/-/node-gtts-2.0.2.tgz",
-      "integrity": "sha512-g1EVgKgUJvbLheD3378SYQpUIca6U9F2VBzMzk5WxxSWzZb+IRGVSMoVGPwvpg4buZxE/FCL13tzzg2j1FAvbQ==",
-      "peer": true,
-      "dependencies": {
-        "async": "^2.4.1",
-        "escape-string-regexp": "^1.0.5",
-        "fake-useragent": "^1.0.1",
-        "multistream": "^2.1.0",
-        "request": "^2.81.0"
-      },
-      "bin": {
-        "node-gtts": "bin.js"
-      }
-    },
-    "node_modules/node-gtts/node_modules/async": {
-      "version": "2.6.4",
-      "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
-      "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
-      "peer": true,
-      "dependencies": {
-        "lodash": "^4.17.14"
-      }
-    },
-    "node_modules/node-gtts/node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "peer": true,
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
     "node_modules/node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -33082,15 +32828,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/oauth-sign": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
-      "peer": true,
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -33982,7 +33719,9 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
       "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
-      "license": "MIT"
+      "dev": true,
+      "license": "MIT",
+      "optional": true
     },
     "node_modules/picocolors": {
       "version": "1.1.1",
@@ -36058,6 +35797,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
       "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/promise-limit": {
@@ -37917,84 +37657,6 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
-    "node_modules/request": {
-      "version": "2.88.2",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
-      "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
-      "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
-      "peer": true,
-      "dependencies": {
-        "aws-sign2": "~0.7.0",
-        "aws4": "^1.8.0",
-        "caseless": "~0.12.0",
-        "combined-stream": "~1.0.6",
-        "extend": "~3.0.2",
-        "forever-agent": "~0.6.1",
-        "form-data": "~2.3.2",
-        "har-validator": "~5.1.3",
-        "http-signature": "~1.2.0",
-        "is-typedarray": "~1.0.0",
-        "isstream": "~0.1.2",
-        "json-stringify-safe": "~5.0.1",
-        "mime-types": "~2.1.19",
-        "oauth-sign": "~0.9.0",
-        "performance-now": "^2.1.0",
-        "qs": "~6.5.2",
-        "safe-buffer": "^5.1.2",
-        "tough-cookie": "~2.5.0",
-        "tunnel-agent": "^0.6.0",
-        "uuid": "^3.3.2"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/request/node_modules/form-data": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-      "peer": true,
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.6",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 0.12"
-      }
-    },
-    "node_modules/request/node_modules/qs": {
-      "version": "6.5.3",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
-      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
-      "peer": true,
-      "engines": {
-        "node": ">=0.6"
-      }
-    },
-    "node_modules/request/node_modules/tough-cookie": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-      "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-      "peer": true,
-      "dependencies": {
-        "psl": "^1.1.28",
-        "punycode": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/request/node_modules/uuid": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
-      "peer": true,
-      "bin": {
-        "uuid": "bin/uuid"
-      }
-    },
     "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -40125,37 +39787,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/sshpk": {
-      "version": "1.18.0",
-      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
-      "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
-      "peer": true,
-      "dependencies": {
-        "asn1": "~0.2.3",
-        "assert-plus": "^1.0.0",
-        "bcrypt-pbkdf": "^1.0.0",
-        "dashdash": "^1.12.0",
-        "ecc-jsbn": "~0.1.1",
-        "getpass": "^0.1.1",
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.0.2",
-        "tweetnacl": "~0.14.0"
-      },
-      "bin": {
-        "sshpk-conv": "bin/sshpk-conv",
-        "sshpk-sign": "bin/sshpk-sign",
-        "sshpk-verify": "bin/sshpk-verify"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/sshpk/node_modules/jsbn": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
-      "peer": true
-    },
     "node_modules/stack-trace": {
       "version": "0.0.10",
       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -41450,12 +41081,6 @@
         "node": "*"
       }
     },
-    "node_modules/tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
-      "peer": true
-    },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -41949,6 +41574,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
       "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/url": {
@@ -42225,26 +41851,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
-      "engines": [
-        "node >=0.6.0"
-      ],
-      "peer": true,
-      "dependencies": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    },
-    "node_modules/verror/node_modules/core-util-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
-      "peer": true
-    },
     "node_modules/vfile": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
diff --git a/package.json b/package.json
index 8fc292651afd..96a6a19b238b 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,6 @@
     "google-auth-library": "^9.7.0",
     "ibm-cloud-sdk-core": "^5.0.2",
     "langfuse": "^3.7.0",
-    "node-gtts": "^2.0.2",
     "node-sql-parser": "^5.2.0",
     "pdf-parse": "^1.1.1",
     "playwright": "^1.47.2",
diff --git a/site/docs/guides/multimodal-red-team.md b/site/docs/guides/multimodal-red-team.md
index c775bca59205..cee442a2df36 100644
--- a/site/docs/guides/multimodal-red-team.md
+++ b/site/docs/guides/multimodal-red-team.md
@@ -34,8 +34,8 @@ npx promptfoo@latest init --example redteam-multi-modal
 # Navigate to the example directory
 cd redteam-multi-modal
 
-# Install dependencies for image and audio strategies
-npm install sharp node-gtts
+# Install required dependencies
+npm install sharp
 
 # Run the static image red team
 npx promptfoo@latest redteam run -c promptfooconfig.static-image.yaml
diff --git a/site/docs/red-team/strategies/audio.md b/site/docs/red-team/strategies/audio.md
index ba5abcac2eb9..9f4b04dddb04 100644
--- a/site/docs/red-team/strategies/audio.md
+++ b/site/docs/red-team/strategies/audio.md
@@ -1,5 +1,7 @@
 ---
 sidebar_label: Audio Inputs
+title: Audio Jailbreaking Strategy
+description: Test how AI systems handle audio-encoded text inputs that may bypass content filters
 ---
 
 # Audio Jailbreaking
@@ -16,17 +18,25 @@ This strategy is useful for:
 
 Use it like so in your promptfooconfig.yaml:
 
-```yaml
+```yaml title="promptfooconfig.yaml"
 strategies:
   - audio
 ```
 
-:::tip
-This strategy requires you to install `node-gtts` for text-to-speech conversion:
+Or with additional configuration:
 
+```yaml
+strategies:
+  - id: audio
+    config:
+      language: fr # Use French audio (ISO 639-1 code)
 ```
-npm i node-gtts
-```
+
+:::warning
+
+This strategy requires remote generation to perform the text-to-speech conversion. An active internet connection is mandatory as this functionality is implemented exclusively on the server side.
+
+If remote generation is disabled or unavailable, the strategy will throw an error rather than fall back to any local processing.
 
 :::
 
@@ -35,12 +45,16 @@ npm i node-gtts
 The strategy performs the following operations:
 
 1. Takes the original text from your test case
-2. Converts the text into speech using Google's Text-to-Speech service
-3. Encodes the MP3 audio as a base64 string
+2. Sends the text to the remote service for conversion to speech audio
+3. Receives the base64-encoded audio data
 4. Replaces the original text in your test case with the base64-encoded audio
 
 The resulting test case contains the same semantic content as the original but in a different format that may be processed differently by AI systems.
 
+## Configuration Options
+
+- `language`: An ISO 639-1 language code to specify which language the text-to-speech system should use. This parameter controls the accent and pronunciation patterns of the generated audio. **Defaults to 'en' (English)** if not specified. Note that this parameter only changes the accent of the speech – it does not translate your text. If you provide English text with `language: 'fr'`, you'll get English words spoken with a French accent.
+
 ## Importance
 
 This strategy is worth implementing because:
@@ -50,4 +64,8 @@ This strategy is worth implementing because:
 3. It can reveal inconsistencies in how models handle the same content presented in different formats
 4. Audio modalities may have different thresholds or processing pipelines for harmful content
 
-For a comprehensive overview of LLM vulnerabilities and red teaming strategies, visit our [Types of LLM Vulnerabilities](/docs/red-team/llm-vulnerability-types) page.
+## See Also
+
+- [Types of LLM Vulnerabilities](/docs/red-team/llm-vulnerability-types)
+- [Red Teaming Strategies](/docs/red-team/strategies)
+- [Multi-Modal Red Teaming Guide](/docs/guides/multimodal-red-team)
diff --git a/src/redteam/strategies/simpleAudio.ts b/src/redteam/strategies/simpleAudio.ts
index 2eabd923b5ec..8ce2cbc0a42a 100644
--- a/src/redteam/strategies/simpleAudio.ts
+++ b/src/redteam/strategies/simpleAudio.ts
@@ -1,79 +1,71 @@
 import { SingleBar, Presets } from 'cli-progress';
+import { fetchWithCache } from '../../cache';
+import { VERSION } from '../../constants';
+import { getUserEmail } from '../../globalConfig/accounts';
 import logger from '../../logger';
+import { REQUEST_TIMEOUT_MS } from '../../providers/shared';
 import type { TestCase } from '../../types';
 import invariant from '../../util/invariant';
-
-// Types for the node-gtts module
-type GttsInstance = {
-  stream(text: string): NodeJS.ReadableStream;
-};
-
-type GttsModule = {
-  default(lang: string): GttsInstance;
-};
+import { getRemoteGenerationUrl, neverGenerateRemote } from '../remoteGeneration';
 
 /**
- * Dynamically imports the node-gtts library
- * @returns The gTTS module or null if not available
+ * Converts text to audio using the remote API
+ * @throws Error if remote generation is disabled or if the API call fails
  */
-async function importGtts(): Promise<GttsModule | null> {
-  try {
-    // Dynamic import of gtts
-    return (await import('node-gtts')) as GttsModule;
-  } catch (error) {
-    logger.warn(`node-gtts library not available: ${error}`);
-    return null;
+export async function textToAudio(text: string, language: string = 'en'): Promise<string> {
+  // Check if remote generation is disabled
+  if (neverGenerateRemote()) {
+    throw new Error(
+      'Remote generation is disabled but required for audio strategy. Please enable remote generation to use this strategy.',
+    );
   }
-}
 
-/**
- * Converts text to an audio stream and then to base64 encoded string
- * using the node-gtts library which provides text-to-speech functionality
- */
-export async function textToAudio(text: string): Promise<string> {
   try {
-    // Dynamically import gtts
-    const gttsModule = await importGtts();
-
-    if (!gttsModule) {
-      throw new Error(
-        `Please install node-gtts to use audio-based strategies: npm install node-gtts`,
-      );
-    }
-
-    // Initialize gtts with English language
-    const gtts = gttsModule.default('en');
-
-    // Get audio stream
-    const audioStream = gtts.stream(text);
+    logger.debug(`Using remote generation for audio task`);
+
+    const payload = {
+      task: 'audio',
+      text,
+      language,
+      version: VERSION,
+      email: getUserEmail(),
+    };
 
-    // Collect stream data in chunks
-    const chunks: Buffer[] = [];
+    const { data } = await fetchWithCache(
+      getRemoteGenerationUrl(),
+      {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(payload),
+      },
+      REQUEST_TIMEOUT_MS,
+    );
 
-    // Convert stream to buffer using promises
-    const buffer = await new Promise<Buffer>((resolve, reject) => {
-      audioStream.on('data', (chunk: Buffer) => chunks.push(chunk));
-      audioStream.on('error', reject);
-      audioStream.on('end', () => resolve(Buffer.concat(chunks)));
-    });
+    if (data.error) {
+      throw new Error(`Error in remote audio generation: ${data.error}`);
+    }
 
-    // Convert to base64
-    return buffer.toString('base64');
+    logger.debug(`Received audio base64 from remote API (${data.audioBase64.length} chars)`);
+    return data.audioBase64;
   } catch (error) {
     logger.error(`Error generating audio from text: ${error}`);
-    // Fallback to base64 encoding of the original text if audio generation fails
-    return Buffer.from(text).toString('base64');
+    throw new Error(
+      `Failed to generate audio: ${error instanceof Error ? error.message : String(error)}. This strategy requires an active internet connection and access to the remote API.`,
+    );
   }
 }
 
 /**
  * Adds audio encoding to test cases
+ * @throws Error if the remote API for audio conversion is unavailable
  */
 export async function addAudioToBase64(
   testCases: TestCase[],
   injectVar: string,
+  config: Record<string, any> = {},
 ): Promise<TestCase[]> {
   const audioTestCases: TestCase[] = [];
+  const language = config.language || 'en';
 
   let progressBar: SingleBar | undefined;
   if (logger.level !== 'debug') {
@@ -95,8 +87,8 @@ export async function addAudioToBase64(
 
     const originalText = String(testCase.vars[injectVar]);
 
-    // Convert text to audio and then to base64
-    const base64Audio = await textToAudio(originalText);
+    // Convert text to audio using the remote API
+    const base64Audio = await textToAudio(originalText, language);
 
     audioTestCases.push({
       ...testCase,
@@ -129,59 +121,3 @@ export async function addAudioToBase64(
 
   return audioTestCases;
 }
-
-// Main function for direct testing via: npx tsx simpleAudio.ts "Text to convert to audio"
-async function main() {
-  // Get text from command line arguments or use default
-  const textToConvert = process.argv[2] || 'This is a test of the audio encoding strategy.';
-
-  logger.info(`Converting text to audio: "${textToConvert}"`);
-
-  try {
-    // Convert text to audio
-    const base64Audio = await textToAudio(textToConvert);
-
-    // Log the first 100 characters of the base64 audio to avoid terminal clutter
-    logger.info(`Base64 audio (first 100 chars): ${base64Audio.substring(0, 100)}...`);
-    logger.info(`Total base64 audio length: ${base64Audio.length} characters`);
-
-    // Create a simple test case
-    const testCase = {
-      vars: {
-        prompt: textToConvert,
-      },
-    };
-
-    // Process the test case
-    const processedTestCases = await addAudioToBase64([testCase], 'prompt');
-
-    logger.info('Test case processed successfully.');
-    logger.info(`Original prompt length: ${textToConvert.length} characters`);
-    // Add type assertion to ensure TypeScript knows this is a string
-    const processedPrompt = processedTestCases[0].vars?.prompt as string;
-    logger.info(`Processed prompt length: ${processedPrompt.length} characters`);
-
-    // Check if we're running this directly (not being imported)
-    if (require.main === module) {
-      // Write to a file for testing with audio players
-      const fs = await import('fs');
-      const outputFilePath = 'test-audio.mp3';
-
-      // Decode base64 back to binary
-      const audioBuffer = Buffer.from(base64Audio, 'base64');
-
-      // Write binary data to file
-      fs.writeFileSync(outputFilePath, audioBuffer);
-
-      logger.info(`Audio file written to: ${outputFilePath}`);
-      logger.info(`You can play it using any audio player to verify the conversion.`);
-    }
-  } catch (error) {
-    logger.error(`Error generating audio from text: ${error}`);
-  }
-}
-
-// Run the main function if this file is executed directly
-if (require.main === module) {
-  main();
-}
diff --git a/src/types/node-gtts.d.ts b/src/types/node-gtts.d.ts
deleted file mode 100644
index ba9a1223d7b9..000000000000
--- a/src/types/node-gtts.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-declare module 'node-gtts' {
-  export default function gtts(lang: string): {
-    stream(text: string): NodeJS.ReadableStream;
-  };
-}
diff --git a/test/redteam/strategies/simpleAudio.test.ts b/test/redteam/strategies/simpleAudio.test.ts
index f758f61041bf..8f02059de288 100644
--- a/test/redteam/strategies/simpleAudio.test.ts
+++ b/test/redteam/strategies/simpleAudio.test.ts
@@ -1,95 +1,242 @@
 import { expect, it, describe } from '@jest/globals';
+import { SingleBar } from 'cli-progress';
+import { fetchWithCache } from '../../../src/cache';
+import logger from '../../../src/logger';
+import { neverGenerateRemote } from '../../../src/redteam/remoteGeneration';
 import { addAudioToBase64, textToAudio } from '../../../src/redteam/strategies/simpleAudio';
 import type { TestCase } from '../../../src/types';
 
+// Mock the remoteGeneration module
+jest.mock('../../../src/redteam/remoteGeneration', () => ({
+  getRemoteGenerationUrl: jest.fn().mockReturnValue('http://test.url'),
+  neverGenerateRemote: jest.fn().mockReturnValue(false),
+}));
+
+// Mock the cache module
+jest.mock('../../../src/cache', () => ({
+  fetchWithCache: jest.fn(),
+}));
+
+// Mock cli-progress
+jest.mock('cli-progress', () => ({
+  Presets: {
+    shades_classic: {},
+  },
+  SingleBar: jest.fn().mockImplementation(() => ({
+    increment: jest.fn(),
+    start: jest.fn(),
+    stop: jest.fn(),
+  })),
+}));
+
 const originalConsoleLog = console.log;
+const mockFetchWithCache = jest.mocked(fetchWithCache);
+const mockNeverGenerateRemote = jest.mocked(neverGenerateRemote);
 
 describe('audio strategy', () => {
   beforeAll(() => {
     jest.spyOn(console, 'log').mockImplementation();
   });
 
+  beforeEach(() => {
+    jest.clearAllMocks();
+    mockFetchWithCache.mockResolvedValue({
+      data: { audioBase64: 'bW9ja2VkLWF1ZGlvLWJhc2U2NC1kYXRh' },
+      cached: false,
+      status: 200,
+      statusText: 'OK',
+    });
+    mockNeverGenerateRemote.mockReturnValue(false);
+  });
+
   afterAll(() => {
     console.log = originalConsoleLog;
   });
 
-  it('textToAudio should convert text to base64 string', async () => {
-    const text = 'Hello, world!';
-    const base64 = await textToAudio(text);
+  describe('textToAudio', () => {
+    it('should convert text to base64 string using remote API', async () => {
+      const text = 'Hello, world!';
+      const base64 = await textToAudio(text, 'en');
+
+      expect(mockFetchWithCache).toHaveBeenCalledWith(
+        expect.any(String),
+        expect.objectContaining({
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: expect.any(String),
+        }),
+        expect.any(Number),
+      );
+      expect(base64).toBe('bW9ja2VkLWF1ZGlvLWJhc2U2NC1kYXRh');
+    });
 
-    // Base64 should be a non-empty string
-    expect(base64).toBeTruthy();
-    expect(typeof base64).toBe('string');
-    expect(base64.length).toBeGreaterThan(0);
+    it('should throw an error if remote generation is disabled', async () => {
+      mockNeverGenerateRemote.mockReturnValue(true);
 
-    // Should be valid base64
-    expect(() => Buffer.from(base64, 'base64')).not.toThrow();
-  }, 15000); // 15 second timeout for potential slow TTS processing
+      const text = 'This should fail';
+      await expect(textToAudio(text, 'en')).rejects.toThrow('Remote generation is disabled');
+    });
 
-  it('addAudioToBase64 should convert test cases with the specified variable', async () => {
-    const testCases: TestCase[] = [
-      {
-        vars: {
-          prompt: 'This is a test prompt',
-          other: 'This should not be changed',
+    it('should throw an error if remote API fails', async () => {
+      mockFetchWithCache.mockRejectedValueOnce(new Error('Remote API error'));
+
+      const text = 'Hello, fallback world!';
+      await expect(textToAudio(text, 'en')).rejects.toThrow('Failed to generate audio');
+    });
+
+    it('should pass language parameter to API', async () => {
+      const text = 'Bonjour, monde!';
+      await textToAudio(text, 'fr');
+
+      // Verify the correct call was made
+      expect(mockFetchWithCache).toHaveBeenCalledWith(
+        expect.any(String),
+        expect.objectContaining({
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: expect.stringContaining('"language":"fr"'),
+        }),
+        expect.any(Number),
+      );
+    });
+  });
+
+  describe('addAudioToBase64', () => {
+    it('should convert test cases with the specified variable', async () => {
+      // Setup mock to return a predictable response
+      mockFetchWithCache.mockResolvedValue({
+        data: { audioBase64: 'bW9ja2VkLWF1ZGlv' },
+        cached: false,
+        status: 200,
+        statusText: 'OK',
+      });
+
+      const testCases: TestCase[] = [
+        {
+          vars: {
+            prompt: 'This is a test prompt',
+            other: 'This should not be changed',
+          },
         },
-      },
-    ];
+      ];
+
+      const result = await addAudioToBase64(testCases, 'prompt');
 
-    const result = await addAudioToBase64(testCases, 'prompt');
+      expect(result).toHaveLength(1);
+      expect(result[0].vars?.prompt).toBe('bW9ja2VkLWF1ZGlv');
+      expect(result[0].vars?.other).toBe('This should not be changed');
+    });
+
+    it('should throw an error when API is unavailable', async () => {
+      mockFetchWithCache.mockRejectedValueOnce(new Error('API unavailable'));
 
-    expect(result).toHaveLength(1);
-    expect(result[0].vars?.prompt).toBeTruthy();
-    expect(typeof result[0].vars?.prompt).toBe('string');
-    expect(result[0].vars?.prompt).not.toBe('This is a test prompt'); // Should be changed
-    expect(result[0].vars?.other).toBe('This should not be changed'); // Should not be changed
+      const testCases: TestCase[] = [
+        {
+          vars: {
+            prompt: 'This should fail',
+          },
+        },
+      ];
 
-    // Should be valid base64
-    expect(() => Buffer.from(result[0].vars?.prompt as string, 'base64')).not.toThrow();
-  }, 15000); // 15 second timeout for potential slow TTS processing
+      await expect(addAudioToBase64(testCases, 'prompt')).rejects.toThrow(
+        'Failed to generate audio',
+      );
+    });
+
+    it('should preserve harmCategory and modify assertion metrics', async () => {
+      const testCase: TestCase = {
+        assert: [{ type: 'promptfoo:redteam:harmful' }],
+        metadata: {
+          harmCategory: 'Illegal Activities',
+          otherField: 'value',
+        },
+        vars: {
+          prompt: 'Harmful content',
+        },
+      };
 
-  it('should preserve harmCategory and modify assertion metrics', async () => {
-    const testCase: TestCase = {
-      assert: [{ type: 'promptfoo:redteam:harmful' }],
-      metadata: {
+      const result = await addAudioToBase64([testCase], 'prompt');
+
+      expect(result[0].metadata).toEqual({
         harmCategory: 'Illegal Activities',
         otherField: 'value',
-      },
-      vars: {
-        prompt: 'Harmful content',
-      },
-    };
-
-    const result = await addAudioToBase64([testCase], 'prompt');
-
-    expect(result[0].metadata).toEqual({
-      harmCategory: 'Illegal Activities',
-      otherField: 'value',
-      strategyId: 'audio',
+        strategyId: 'audio',
+      });
+      expect(result[0].assert).toEqual([
+        {
+          metric: 'harmful/Audio-Encoded',
+          type: 'promptfoo:redteam:harmful',
+        },
+      ]);
     });
-    expect(result[0].assert).toEqual([
-      {
-        metric: 'harmful/Audio-Encoded',
-        type: 'promptfoo:redteam:harmful',
-      },
-    ]);
-  }, 15000); // 15 second timeout for potential slow TTS processing
-
-  it('should handle test cases without metadata or assertions', async () => {
-    const testCase: TestCase = {
-      vars: {
-        prompt: 'Simple content',
-      },
-    };
-
-    const result = await addAudioToBase64([testCase], 'prompt');
-
-    expect(result[0].metadata).toEqual({
-      strategyId: 'audio',
+
+    it('should handle test cases without metadata or assertions', async () => {
+      const testCase: TestCase = {
+        vars: {
+          prompt: 'Simple content',
+        },
+      };
+
+      const result = await addAudioToBase64([testCase], 'prompt');
+
+      expect(result[0].metadata).toEqual({
+        strategyId: 'audio',
+      });
+      expect(result[0].assert).toBeUndefined();
+    });
+
+    it('should use language from config if provided', async () => {
+      const testCase: TestCase = {
+        vars: {
+          prompt: 'This should be in Spanish',
+        },
+      };
+
+      await addAudioToBase64([testCase], 'prompt', { language: 'es' });
+
+      expect(mockFetchWithCache).toHaveBeenCalledWith(
+        expect.any(String),
+        expect.objectContaining({
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: expect.stringContaining('"language":"es"'),
+        }),
+        expect.any(Number),
+      );
     });
-    expect(result[0].assert).toBeUndefined();
 
-    // Just check it's a valid base64 string
-    expect(() => Buffer.from(result[0].vars?.prompt as string, 'base64')).not.toThrow();
-  }, 15000); // 15 second timeout for potential slow TTS processing
+    it('should use progress bar when logger level is not debug', async () => {
+      const testCase: TestCase = {
+        vars: {
+          prompt: 'Test progress bar',
+        },
+      };
+
+      // Save original level
+      const originalLevel = logger.level;
+      // Set level to info to enable progress bar
+      logger.level = 'info';
+
+      // Create mock for SingleBar
+      const mockBarInstance = {
+        increment: jest.fn(),
+        start: jest.fn(),
+        stop: jest.fn(),
+      };
+
+      // Cast SingleBar to any to avoid TypeScript errors with mocking
+      const mockSingleBar = SingleBar as any;
+      const originalImplementation = mockSingleBar.mockImplementation;
+      mockSingleBar.mockImplementation(() => mockBarInstance);
+
+      await addAudioToBase64([testCase], 'prompt');
+
+      expect(mockBarInstance.increment).toHaveBeenCalledWith(1);
+      expect(mockBarInstance.stop).toHaveBeenCalledWith();
+
+      // Restore original implementation and logger level
+      mockSingleBar.mockImplementation = originalImplementation;
+      logger.level = originalLevel;
+    });
+  });
 });


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T06:31:19Z
user: login: sklein12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:27:48Z
user: login: MrFlounder
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T00:53:26Z

Review comments:
diff_hunk: @@ -1,79 +1,71 @@
 import { SingleBar, Presets } from 'cli-progress';
+import { fetchWithCache } from '../../cache';
+import { VERSION } from '../../constants';
+import { getUserEmail } from '../../globalConfig/accounts';
 import logger from '../../logger';
+import { REQUEST_TIMEOUT_MS } from '../../providers/shared';
 import type { TestCase } from '../../types';
 import invariant from '../../util/invariant';
-
-// Types for the node-gtts module
-type GttsInstance = {
-  stream(text: string): NodeJS.ReadableStream;
-};
-
-type GttsModule = {
-  default(lang: string): GttsInstance;
-};
+import { getRemoteGenerationUrl, neverGenerateRemote } from '../remoteGeneration';
 
 /**
- * Dynamically imports the node-gtts library
- * @returns The gTTS module or null if not available
+ * Converts text to audio using the remote API
+ * @throws Error if remote generation is disabled or if the API call fails
  */
-async function importGtts(): Promise<GttsModule | null> {
-  try {
-    // Dynamic import of gtts
-    return (await import('node-gtts')) as GttsModule;
-  } catch (error) {
-    logger.warn(`node-gtts library not available: ${error}`);
-    return null;
+export async function textToAudio(text: string, language: string = 'en'): Promise<string> {
+  // Check if remote generation is disabled
+  if (neverGenerateRemote()) {
+    throw new Error(
+      'Remote generation is disabled but required for audio strategy. Please enable remote generation to use this strategy.',
+    );
   }
-}
 
-/**
- * Converts text to an audio stream and then to base64 encoded string
- * using the node-gtts library which provides text-to-speech functionality
- */
-export async function textToAudio(text: string): Promise<string> {
   try {
-    // Dynamically import gtts
-    const gttsModule = await importGtts();
-
-    if (!gttsModule) {
-      throw new Error(
-        `Please install node-gtts to use audio-based strategies: npm install node-gtts`,
-      );
-    }
-
-    // Initialize gtts with English language
-    const gtts = gttsModule.default('en');
-
-    // Get audio stream
-    const audioStream = gtts.stream(text);
+    logger.debug(`Using remote generation for audio task`);
+
+    const payload = {
+      task: 'audio',
+      text,
+      language,
+      version: VERSION,
+      email: getUserEmail(),
+    };
 
-    // Collect stream data in chunks
-    const chunks: Buffer[] = [];
+    const { data } = await fetchWithCache(
+      getRemoteGenerationUrl(),
+      {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(payload),
+      },
+      REQUEST_TIMEOUT_MS,
+    );
 
-    // Convert stream to buffer using promises
-    const buffer = await new Promise<Buffer>((resolve, reject) => {
-      audioStream.on('data', (chunk: Buffer) => chunks.push(chunk));
-      audioStream.on('error', reject);
-      audioStream.on('end', () => resolve(Buffer.concat(chunks)));
-    });
+    if (data.error) {
+      throw new Error(`Error in remote audio generation: ${data.error}`);
+    }
 
-    // Convert to base64
-    return buffer.toString('base64');
+    logger.debug(`Received audio base64 from remote API (${data.audioBase64.length} chars)`);
path: src/redteam/strategies/simpleAudio.ts
user: login: ellipsis-dev[bot]
body: Consider adding a sanity check for `data.audioBase64` before using its length. If the API response is malformed or missing this field, accessing `.length` may cause a runtime error.
```suggestion
    logger.debug(`Received audio base64 from remote API (${data.audioBase64 ? data.audioBase64.length : 'unknown'} chars)`);
```
created_at: 2025-04-08T06:31:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,17 +18,25 @@ This strategy is useful for:
 
 Use it like so in your promptfooconfig.yaml:
 
-```yaml
+```yaml title="promptfooconfig.yaml"
 strategies:
   - audio
 ```
 
-:::tip
-This strategy requires you to install `node-gtts` for text-to-speech conversion:
+Or with additional configuration:
 
+```yaml
+strategies:
+  - id: audio
+    config:
+      language: fr # Use French audio (ISO 639-1 code)
 ```
-npm i node-gtts
-```
+
+:::warning
+
+This strategy requires remote generation to perform the text-to-speech conversion. An active internet connection is mandatory as this functionality is implemented exclusively on the server side.
+
path: site/docs/red-team/strategies/audio.md
user: login: sklein12
body: maybe be more specific "This requires remote generation for promptfoo-cloud"?
created_at: 2025-04-08T19:27:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: gru-agent[bot]
created_at: 2025-04-08T06:29:58Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/44a95e4a-2883-4066-bd1d-9379529c1215) | c5a775e3647c6ffab9204ac7c0661574b59d99b2 | 🚫 Skipped | No files need to be tested {"package-lock.json":"target file(package-lock.json) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","package.json":"target file(package.json) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","site/docs/guides/multimodal-red-team.md":"target file(site/docs/guides/multimodal-red-team.md) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","site/docs/red-team/strategies/audio.md":"target file(site/docs/red-team/strategies/audio.md) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","src/redteam/strategies/simpleAudio.ts":… |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR
user: login: gru-agent[bot]
created_at: 2025-04-08T06:30:01Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/dc90242c-5ef7-4022-bc73-755b90695ea8) | c5a775e3647c6ffab9204ac7c0661574b59d99b2 | 🚫 Skipped | No files need to be tested {"package-lock.json":"target file(package-lock.json) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","package.json":"target file(package.json) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","site/docs/guides/multimodal-red-team.md":"target file(site/docs/guides/multimodal-red-team.md) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","site/docs/red-team/strategies/audio.md":"target file(site/docs/red-team/strategies/audio.md) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","src/redteam/strategies/… |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR