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>
This commit is contained in:
Dave Horton
2026-08-08 22:49:50 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent d89457f67d
commit c066840f85
2 changed files with 51 additions and 2 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 {
+48
View File
@@ -1147,6 +1147,54 @@ test('inworld speech synth', async(t) => {
client.quit();
});
test('inworld streaming say: params', async(t) => {
const fn = require('..');
const {synthAudio, client} = fn(opts, logger);
/* the streaming branch builds the say: path without calling the vendor,
so this needs no credentials
*/
try {
let result = await synthAudio(stats, {
vendor: 'inworld',
credentials: {api_key: 'test-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: 'test-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);