From f859a7ed5b8b69f24f551a9cda4400cc053033fc Mon Sep 17 00:00:00 2001 From: Blackframe AI <122847831+BlackFrameAI@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:42:02 -0700 Subject: [PATCH] fix(stage-ui): correct signed SSML prosody percentages (#1043) --- .../src/stores/modules/speech.test.ts | 18 +++++++++++++ .../stage-ui/src/stores/modules/speech.ts | 26 ++++++++++++------- 2 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 packages/stage-ui/src/stores/modules/speech.test.ts diff --git a/packages/stage-ui/src/stores/modules/speech.test.ts b/packages/stage-ui/src/stores/modules/speech.test.ts new file mode 100644 index 000000000..23f94c0d1 --- /dev/null +++ b/packages/stage-ui/src/stores/modules/speech.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import { toSignedPercent } from './speech' + +describe('speech store helpers', () => { + it('formats positive percentages with a plus sign', () => { + expect(toSignedPercent(25)).toBe('+25%') + }) + + it('formats negative percentages without a double minus', () => { + expect(toSignedPercent(-20)).toBe('-20%') + expect(toSignedPercent(-20)).not.toContain('--') + }) + + it('formats zero as 0%', () => { + expect(toSignedPercent(0)).toBe('0%') + }) +}) diff --git a/packages/stage-ui/src/stores/modules/speech.ts b/packages/stage-ui/src/stores/modules/speech.ts index db07601f7..b8c9c040f 100644 --- a/packages/stage-ui/src/stores/modules/speech.ts +++ b/packages/stage-ui/src/stores/modules/speech.ts @@ -12,6 +12,14 @@ import { x } from 'xastscript' import { useProvidersStore } from '../providers' +export function toSignedPercent(value: number): string { + if (value > 0) + return `+${value}%` + if (value < 0) + return `-${Math.abs(value)}%` + return '0%' +} + export const useSpeechStore = defineStore('speech', () => { const providersStore = useProvidersStore() const { allAudioSpeechProvidersMetadata } = storeToRefs(providersStore) @@ -192,9 +200,7 @@ export const useSpeechStore = defineStore('speech', () => { const prosody = { pitch: pitch != null - ? pitch > 0 - ? `+${pitch}%` - : `-${pitch}%` + ? toSignedPercent(pitch) : undefined, rate: speed != null ? speed !== 1.0 @@ -202,19 +208,19 @@ export const useSpeechStore = defineStore('speech', () => { : '1' : undefined, volume: volume != null - ? volume > 0 - ? `+${volume}%` - : `${volume}%` + ? toSignedPercent(volume) : undefined, } + const hasProsody = Object.values(prosody).some(value => value != null) + const ssmlXast = x('speak', { 'version': '1.0', 'xmlns': 'http://www.w3.org/2001/10/synthesis', 'xml:lang': voice.languages[0]?.code || 'en-US' }, [ x('voice', { name: voice.id, gender: voice.gender || 'neutral' }, [ - Object.entries(prosody).filter(([_, value]) => value != null).length > 0 + hasProsody ? x('prosody', { - pitch: pitch != null ? pitch > 0 ? `+${pitch}%` : `-${pitch}%` : undefined, - rate: speed != null ? speed !== 1.0 ? `${speed}` : '1' : undefined, - volume: volume != null ? volume > 0 ? `+${volume}%` : `${volume}%` : undefined, + pitch: prosody.pitch, + rate: prosody.rate, + volume: prosody.volume, }, [ text, ])