Compare commits

..
3 Commits
Author SHA1 Message Date
Dave Horton 33456b93d9 1.0.16 2026-08-08 23:07:43 -04:00
Dave HortonandClaude Opus 5 70e91b5057 test(inworld): gate the say: param test on INWORLD_API_KEY (#156)
The test I added in #155 ran unconditionally, which broke `npm test` without
credentials — the path husky's pre-commit hook takes, so `npm version patch`
could not commit.

Two causes, both addressed:

- no credential gate, unlike every other vendor test in this file. Now skips
  without INWORLD_API_KEY, and closes its redis client on that path so the
  run can still exit.
- it assumed streaming was enabled. The Google non-streaming test sets
  JAMBONES_DISABLE_TTS_STREAMING and, on its no-credentials skip path,
  deletes the env var WITHOUT clearing the require cache (unlike its finally
  block, which clears both) — so lib/config still held 'true' further down
  the file and synthInworld took the non-streaming branch, attempting a real
  vendor call. The test now re-requires with streaming enabled so it does not
  depend on what ran before it.

Verified both ways: skips and exits 0 with no key; 11/11 with a key even
under the leaked state. Re-introducing the #155 bug still fails 3 assertions,
so the regression value is intact.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 23:06:52 -04:00
Dave HortonandClaude Opus 5 c066840f85 fix(inworld): read pitch and speakingRate from audioConfig in the say: params (#155)
The streaming say: path guarded on opts.audioConfig?.pitch and
opts.audioConfig?.speakingRate but interpolated opts.pitch and
opts.speakingRate, which are undefined — so anyone setting them under
audioConfig (what the docs and the portal defaults tell you to do) got
'pitch=undefined,speakingRate=undefined' on the wire and their setting
silently dropped.

Adds a test for the say: params that needs no credentials, since the
streaming branch builds the path without calling the vendor.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:49:50 -04:00
4 changed files with 69 additions and 5 deletions
+3 -2
View File
@@ -941,8 +941,9 @@ const synthInworld = async(logger, {
params += `,voice=${voice}`;
params += `,write_cache_file=${disableTtsCache ? 0 : 1}`;
if (opts.temperature) params += `,temperature=${opts.temperature}`;
if (opts.audioConfig?.pitch) params += `,pitch=${opts.pitch}`;
if (opts.audioConfig?.speakingRate) params += `,speakingRate=${opts.speakingRate}`;
/* pitch and speakingRate are nested under audioConfig, matching Inworld's API */
if (opts.audioConfig?.pitch) params += `,pitch=${opts.audioConfig.pitch}`;
if (opts.audioConfig?.speakingRate) params += `,speakingRate=${opts.audioConfig.speakingRate}`;
params += '}';
return {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@jambonz/speech-utils",
"version": "1.0.15",
"version": "1.0.16",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@jambonz/speech-utils",
"version": "1.0.15",
"version": "1.0.16",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-polly": "^3.496.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@jambonz/speech-utils",
"version": "1.0.15",
"version": "1.0.16",
"description": "TTS-related speech utilities for jambonz",
"main": "index.js",
"author": "Dave Horton",
+63
View File
@@ -1147,6 +1147,69 @@ test('inworld speech synth', async(t) => {
client.quit();
});
test('inworld streaming say: params', async(t) => {
/* This test asserts the streaming say: path, so it must run with streaming
enabled. The Google non-streaming test above sets
JAMBONES_DISABLE_TTS_STREAMING and, on its no-credentials skip path,
deletes the env var WITHOUT clearing the require cache — so lib/config can
still be holding 'true' by the time we get here. Re-require to be
independent of what ran before us.
*/
delete process.env.JAMBONES_DISABLE_TTS_STREAMING;
delete require.cache[require.resolve('../lib/config')];
delete require.cache[require.resolve('../lib/synth-audio')];
delete require.cache[require.resolve('..')];
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);
if (!process.env.INWORLD_API_KEY) {
t.pass('skipping inworld streaming say: param tests since INWORLD_API_KEY is not provided');
client.quit();
return t.end();
}
try {
let result = await synthAudio(stats, {
vendor: 'inworld',
credentials: {api_key: process.env.INWORLD_API_KEY, model_id: 'inworld-tts-1.5-mini'},
language: 'en',
voice: 'Ashley',
text: 'This is a test of inworld streaming.',
options: {temperature: 0.9, audioConfig: {pitch: 2.5, speakingRate: 1.2}},
disableTtsCache: true
});
t.ok(result.filePath.startsWith('say:'), 'inworld returns streaming say: path');
t.ok(result.filePath.includes('vendor=inworld'), 'streaming path contains vendor=inworld');
t.ok(result.filePath.includes('voice=Ashley'), 'streaming path contains voice');
t.ok(result.filePath.includes('model_id=inworld-tts-1.5-mini'), 'streaming path contains model_id');
t.ok(result.filePath.includes('temperature=0.9'), 'streaming path contains temperature');
/* pitch and speakingRate are nested under audioConfig; they used to be read
from the top level and emitted as "undefined"
*/
t.ok(result.filePath.includes('speakingRate=1.2'), 'audioConfig.speakingRate reaches the say: params');
t.ok(result.filePath.includes('pitch=2.5'), 'audioConfig.pitch reaches the say: params');
t.ok(!result.filePath.includes('undefined'), 'no undefined values in the say: params');
/* options omitted entirely: no stray keys */
result = await synthAudio(stats, {
vendor: 'inworld',
credentials: {api_key: process.env.INWORLD_API_KEY, model_id: 'inworld-tts-1.5-mini'},
language: 'en',
voice: 'Ashley',
text: 'This is a test of inworld streaming.',
disableTtsCache: true
});
t.ok(!result.filePath.includes('speakingRate='), 'speakingRate omitted when unset');
t.ok(!result.filePath.includes('pitch='), 'pitch omitted when unset');
t.ok(!result.filePath.includes('undefined'), 'no undefined values when options are omitted');
} catch (err) {
console.error(JSON.stringify(err));
t.end(err);
}
client.quit();
});
test('resemble speech synth', async(t) => {
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);