Skip to content

Commit 88cf106

Browse files
committed
Add speed config option to text-to-speech
1 parent 1a47e9d commit 88cf106

File tree

9 files changed

+134
-3
lines changed

9 files changed

+134
-3
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { McpServerManager } from "../../services/mcp/McpServerManager"
3232
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
3333
import { fileExistsAtPath } from "../../utils/fs"
3434
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
35-
import { playTts, setTtsEnabled } from "../../utils/tts"
35+
import { playTts, setTtsEnabled, setTtsSpeed } from "../../utils/tts"
3636
import { singleCompletionHandler } from "../../utils/single-completion-handler"
3737
import { searchCommits } from "../../utils/git"
3838
import { getDiffStrategy } from "../diff/DiffStrategy"
@@ -1252,6 +1252,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12521252
setTtsEnabled(ttsEnabled) // Add this line to update the tts utility
12531253
await this.postStateToWebview()
12541254
break
1255+
case "ttsSpeed":
1256+
const ttsSpeed = message.value ?? 1.0
1257+
await this.updateGlobalState("ttsSpeed", ttsSpeed)
1258+
setTtsSpeed(ttsSpeed)
1259+
await this.postStateToWebview()
1260+
break
12551261
case "playTts":
12561262
if (message.text) {
12571263
playTts(message.text)
@@ -2196,6 +2202,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21962202
alwaysAllowModeSwitch,
21972203
soundEnabled,
21982204
ttsEnabled,
2205+
ttsSpeed,
21992206
diffEnabled,
22002207
enableCheckpoints,
22012208
checkpointStorage,
@@ -2252,6 +2259,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22522259
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
22532260
soundEnabled: soundEnabled ?? false,
22542261
ttsEnabled: ttsEnabled ?? false,
2262+
ttsSpeed: ttsSpeed ?? 1.0,
22552263
diffEnabled: diffEnabled ?? true,
22562264
enableCheckpoints: enableCheckpoints ?? true,
22572265
checkpointStorage: checkpointStorage ?? "task",
@@ -2408,6 +2416,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
24082416
allowedCommands: stateValues.allowedCommands,
24092417
soundEnabled: stateValues.soundEnabled ?? false,
24102418
ttsEnabled: stateValues.ttsEnabled ?? false,
2419+
ttsSpeed: stateValues.ttsSpeed ?? 1.0,
24112420
diffEnabled: stateValues.diffEnabled ?? true,
24122421
enableCheckpoints: stateValues.enableCheckpoints ?? true,
24132422
checkpointStorage: stateValues.checkpointStorage ?? "task",

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export interface ExtensionState {
117117
allowedCommands?: string[]
118118
soundEnabled?: boolean
119119
ttsEnabled?: boolean
120+
ttsSpeed?: number
120121
soundVolume?: number
121122
diffEnabled?: boolean
122123
enableCheckpoints: boolean

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface WebviewMessage {
5252
| "playTts"
5353
| "soundEnabled"
5454
| "ttsEnabled"
55+
| "ttsSpeed"
5556
| "soundVolume"
5657
| "diffEnabled"
5758
| "enableCheckpoints"

src/shared/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const GLOBAL_STATE_KEYS = [
6060
"allowedCommands",
6161
"soundEnabled",
6262
"ttsEnabled",
63+
"ttsSpeed",
6364
"soundVolume",
6465
"diffEnabled",
6566
"enableCheckpoints",

src/utils/tts.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode"
22

33
let isTtsEnabled = false
4+
let speed = 1.0
45
let isSpeaking = false
56
const utteranceQueue: string[] = []
67

@@ -12,6 +13,14 @@ export const setTtsEnabled = (enabled: boolean): void => {
1213
isTtsEnabled = enabled
1314
}
1415

16+
/**
17+
* Set tts speed
18+
* @param speed number
19+
*/
20+
export const setTtsSpeed = (newSpeed: number): void => {
21+
speed = newSpeed
22+
}
23+
1524
/**
1625
* Process the next item in the utterance queue
1726
*/
@@ -27,7 +36,7 @@ const processQueue = async (): Promise<void> => {
2736

2837
// Wrap say.speak in a promise to handle completion
2938
await new Promise<void>((resolve, reject) => {
30-
say.speak(nextUtterance, null, null, (err: Error) => {
39+
say.speak(nextUtterance, null, speed, (err: Error) => {
3140
if (err) {
3241
reject(err)
3342
} else {

webview-ui/src/components/settings/NotificationSettings.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { Section } from "./Section"
88

99
type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
1010
ttsEnabled?: boolean
11+
ttsSpeed?: number
1112
soundEnabled?: boolean
1213
soundVolume?: number
13-
setCachedStateField: SetCachedStateField<"ttsEnabled" | "soundEnabled" | "soundVolume">
14+
setCachedStateField: SetCachedStateField<"ttsEnabled" | "ttsSpeed" | "soundEnabled" | "soundVolume">
1415
}
1516

1617
export const NotificationSettings = ({
1718
ttsEnabled,
19+
ttsSpeed,
1820
soundEnabled,
1921
soundVolume,
2022
setCachedStateField,
@@ -39,6 +41,31 @@ export const NotificationSettings = ({
3941
<p className="text-vscode-descriptionForeground text-sm mt-0">
4042
When enabled, Roo will read aloud its responses using text-to-speech.
4143
</p>
44+
{ttsEnabled && (
45+
<div
46+
style={{
47+
marginLeft: 0,
48+
paddingLeft: 10,
49+
borderLeft: "2px solid var(--vscode-button-background)",
50+
}}>
51+
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
52+
<input
53+
type="range"
54+
min="0.1"
55+
max="2.0"
56+
step="0.01"
57+
value={ttsSpeed ?? 1.0}
58+
onChange={(e) => setCachedStateField("ttsSpeed", parseFloat(e.target.value))}
59+
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
60+
aria-label="Speed"
61+
/>
62+
<span style={{ minWidth: "35px", textAlign: "left" }}>
63+
{((ttsSpeed ?? 1.0) * 100).toFixed(0)}%
64+
</span>
65+
</div>
66+
<p className="text-vscode-descriptionForeground text-sm mt-1">Speed</p>
67+
</div>
68+
)}
4269
</div>
4370
<div>
4471
<VSCodeCheckbox

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
7979
screenshotQuality,
8080
soundEnabled,
8181
ttsEnabled,
82+
ttsSpeed,
8283
soundVolume,
8384
telemetrySetting,
8485
terminalOutputLimit,
@@ -168,6 +169,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
168169
vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled })
169170
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
170171
vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled })
172+
vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed })
171173
vscode.postMessage({ type: "soundVolume", value: soundVolume })
172174
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
173175
vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
@@ -392,6 +394,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
392394
<div ref={notificationsRef}>
393395
<NotificationSettings
394396
ttsEnabled={ttsEnabled}
397+
ttsSpeed={ttsSpeed}
395398
soundEnabled={soundEnabled}
396399
soundVolume={soundVolume}
397400
setCachedStateField={setCachedStateField}

webview-ui/src/components/settings/__tests__/SettingsView.test.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ const mockPostMessage = (state: any) => {
106106
shouldShowAnnouncement: false,
107107
allowedCommands: [],
108108
alwaysAllowExecute: false,
109+
ttsEnabled: false,
110+
ttsSpeed: 1.0,
109111
soundEnabled: false,
110112
soundVolume: 0.5,
111113
...state,
@@ -132,6 +134,18 @@ describe("SettingsView - Sound Settings", () => {
132134
jest.clearAllMocks()
133135
})
134136

137+
it("initializes with tts disabled by default", () => {
138+
renderSettingsView()
139+
140+
const ttsCheckbox = screen.getByRole("checkbox", {
141+
name: /Enable text-to-speech/i,
142+
})
143+
expect(ttsCheckbox).not.toBeChecked()
144+
145+
// Speed slider should not be visible when tts is disabled
146+
expect(screen.queryByRole("slider", { name: /speed/i })).not.toBeInTheDocument()
147+
})
148+
135149
it("initializes with sound disabled by default", () => {
136150
renderSettingsView()
137151

@@ -144,6 +158,29 @@ describe("SettingsView - Sound Settings", () => {
144158
expect(screen.queryByRole("slider", { name: /volume/i })).not.toBeInTheDocument()
145159
})
146160

161+
it("toggles tts setting and sends message to VSCode", () => {
162+
renderSettingsView()
163+
164+
const ttsCheckbox = screen.getByRole("checkbox", {
165+
name: /Enable text-to-speech/i,
166+
})
167+
168+
// Enable tts
169+
fireEvent.click(ttsCheckbox)
170+
expect(ttsCheckbox).toBeChecked()
171+
172+
// Click Save to save settings
173+
const saveButton = screen.getByText("Save")
174+
fireEvent.click(saveButton)
175+
176+
expect(vscode.postMessage).toHaveBeenCalledWith(
177+
expect.objectContaining({
178+
type: "ttsEnabled",
179+
bool: true,
180+
}),
181+
)
182+
})
183+
147184
it("toggles sound setting and sends message to VSCode", () => {
148185
renderSettingsView()
149186

@@ -167,6 +204,21 @@ describe("SettingsView - Sound Settings", () => {
167204
)
168205
})
169206

207+
it("shows tts slider when sound is enabled", () => {
208+
renderSettingsView()
209+
210+
// Enable tts
211+
const ttsCheckbox = screen.getByRole("checkbox", {
212+
name: /Enable text-to-speech/i,
213+
})
214+
fireEvent.click(ttsCheckbox)
215+
216+
// Speed slider should be visible
217+
const speedSlider = screen.getByRole("slider", { name: /speed/i })
218+
expect(speedSlider).toBeInTheDocument()
219+
expect(speedSlider).toHaveValue("1.0")
220+
})
221+
170222
it("shows volume slider when sound is enabled", () => {
171223
renderSettingsView()
172224

@@ -182,6 +234,30 @@ describe("SettingsView - Sound Settings", () => {
182234
expect(volumeSlider).toHaveValue("0.5")
183235
})
184236

237+
it("updates speed and sends message to VSCode when slider changes", () => {
238+
renderSettingsView()
239+
240+
// Enable tts
241+
const ttsCheckbox = screen.getByRole("checkbox", {
242+
name: /Enable text-to-speech/i,
243+
})
244+
fireEvent.click(ttsCheckbox)
245+
246+
// Change speed
247+
const speedSlider = screen.getByRole("slider", { name: /speed/i })
248+
fireEvent.change(speedSlider, { target: { value: "0.75" } })
249+
250+
// Click Save to save settings
251+
const saveButton = screen.getByText("Save")
252+
fireEvent.click(saveButton)
253+
254+
// Verify message sent to VSCode
255+
expect(vscode.postMessage).toHaveBeenCalledWith({
256+
type: "ttsSpeed",
257+
value: 0.75,
258+
})
259+
})
260+
185261
it("updates volume and sends message to VSCode when slider changes", () => {
186262
renderSettingsView()
187263

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface ExtensionStateContextType extends ExtensionState {
3838
setSoundEnabled: (value: boolean) => void
3939
setSoundVolume: (value: number) => void
4040
setTtsEnabled: (value: boolean) => void
41+
setTtsSpeed: (value: number) => void
4142
setDiffEnabled: (value: boolean) => void
4243
setEnableCheckpoints: (value: boolean) => void
4344
setBrowserViewportSize: (value: string) => void
@@ -114,6 +115,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
114115
soundEnabled: false,
115116
soundVolume: 0.5,
116117
ttsEnabled: false,
118+
ttsSpeed: 1.0,
117119
diffEnabled: false,
118120
enableCheckpoints: true,
119121
checkpointStorage: "task",
@@ -229,6 +231,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
229231
filePaths,
230232
openedTabs,
231233
soundVolume: state.soundVolume,
234+
ttsSpeed: state.ttsSpeed,
232235
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
233236
writeDelayMs: state.writeDelayMs,
234237
screenshotQuality: state.screenshotQuality,
@@ -254,6 +257,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
254257
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
255258
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
256259
setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })),
260+
setTtsSpeed: (value) => setState((prevState) => ({ ...prevState, ttsSpeed: value })),
257261
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
258262
setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
259263
setBrowserViewportSize: (value: string) =>

0 commit comments

Comments
 (0)