Skip to content

Commit 04f554b

Browse files
authored
speech - scaffold keyword activation (microsoft#202643)
* speech - scaffold keyword activation * first cut settting for keyword activation * handle lifecycle better * . * . * . * tweaks * show a status bar entry * add in context option * cleanup
1 parent 3530c3b commit 04f554b

File tree

16 files changed

+514
-103
lines changed

16 files changed

+514
-103
lines changed

src/vs/base/common/event.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -622,27 +622,6 @@ export namespace Event {
622622
return event(e => handler(e));
623623
}
624624

625-
/**
626-
* Adds a listener to an event and calls the listener immediately with undefined as the event object. A new
627-
* {@link DisposableStore} is passed to the listener which is disposed when the returned disposable is disposed.
628-
*/
629-
export function runAndSubscribeWithStore<T>(event: Event<T>, handler: (e: T | undefined, disposableStore: DisposableStore) => any): IDisposable {
630-
let store: DisposableStore | null = null;
631-
632-
function run(e: T | undefined) {
633-
store?.dispose();
634-
store = new DisposableStore();
635-
handler(e, store);
636-
}
637-
638-
run(undefined);
639-
const disposable = event(e => run(e));
640-
return toDisposable(() => {
641-
disposable.dispose();
642-
store?.dispose();
643-
});
644-
}
645-
646625
class EmitterObserver<T> implements IObserver {
647626

648627
readonly emitter: Emitter<T>;

src/vs/base/test/common/event.test.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { DeferredPromise, timeout } from 'vs/base/common/async';
88
import { CancellationToken } from 'vs/base/common/cancellation';
99
import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors';
1010
import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event';
11-
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, toDisposable, DisposableTracker } from 'vs/base/common/lifecycle';
11+
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, DisposableTracker } from 'vs/base/common/lifecycle';
1212
import { observableValue, transaction } from 'vs/base/common/observable';
1313
import { MicrotaskDelay } from 'vs/base/common/symbols';
1414
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
@@ -1272,36 +1272,6 @@ suite('Event utils', () => {
12721272
});
12731273
});
12741274

1275-
test('runAndSubscribeWithStore', () => {
1276-
const eventEmitter = ds.add(new Emitter());
1277-
const event = eventEmitter.event;
1278-
1279-
let i = 0;
1280-
const log = new Array<any>();
1281-
const disposable = Event.runAndSubscribeWithStore(event, (e, disposables) => {
1282-
const idx = i++;
1283-
log.push({ label: 'handleEvent', data: e || null, idx });
1284-
disposables.add(toDisposable(() => {
1285-
log.push({ label: 'dispose', idx });
1286-
}));
1287-
});
1288-
1289-
log.push({ label: 'fire' });
1290-
eventEmitter.fire('someEventData');
1291-
1292-
log.push({ label: 'disposeAll' });
1293-
disposable.dispose();
1294-
1295-
assert.deepStrictEqual(log, [
1296-
{ label: 'handleEvent', data: null, idx: 0 },
1297-
{ label: 'fire' },
1298-
{ label: 'dispose', idx: 0 },
1299-
{ label: 'handleEvent', data: 'someEventData', idx: 1 },
1300-
{ label: 'disposeAll' },
1301-
{ label: 'dispose', idx: 1 },
1302-
]);
1303-
});
1304-
13051275
suite('accumulate', () => {
13061276
test('should not fire after a listener is disposed with undefined or []', async () => {
13071277
const eventEmitter = ds.add(new Emitter<number>());

src/vs/workbench/api/browser/mainThreadSpeech.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@ import { Emitter } from 'vs/base/common/event';
88
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { ExtHostContext, ExtHostSpeechShape, MainContext, MainThreadSpeechShape } from 'vs/workbench/api/common/extHost.protocol';
11-
import { ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService';
11+
import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService';
1212
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
1313

1414
type SpeechToTextSession = {
1515
readonly onDidChange: Emitter<ISpeechToTextEvent>;
1616
};
1717

18+
type KeywordRecognitionSession = {
19+
readonly onDidChange: Emitter<IKeywordRecognitionEvent>;
20+
};
21+
1822
@extHostNamedCustomer(MainContext.MainThreadSpeech)
1923
export class MainThreadSpeech extends Disposable implements MainThreadSpeechShape {
2024

2125
private readonly proxy: ExtHostSpeechShape;
2226

2327
private readonly providerRegistrations = new Map<number, IDisposable>();
24-
private readonly providerSessions = new Map<number, SpeechToTextSession>();
28+
29+
private readonly speechToTextSessions = new Map<number, SpeechToTextSession>();
30+
private readonly keywordRecognitionSessions = new Map<number, KeywordRecognitionSession>();
2531

2632
constructor(
2733
extHostContext: IExtHostContext,
@@ -47,13 +53,33 @@ export class MainThreadSpeech extends Disposable implements MainThreadSpeechShap
4753
disposables.add(token.onCancellationRequested(() => this.proxy.$cancelSpeechToTextSession(session)));
4854

4955
const onDidChange = disposables.add(new Emitter<ISpeechToTextEvent>());
50-
this.providerSessions.set(session, { onDidChange });
56+
this.speechToTextSessions.set(session, { onDidChange });
5157

5258
return {
5359
onDidChange: onDidChange.event,
5460
dispose: () => {
5561
cts.dispose(true);
56-
this.providerSessions.delete(session);
62+
this.speechToTextSessions.delete(session);
63+
disposables.dispose();
64+
}
65+
};
66+
},
67+
createKeywordRecognitionSession: token => {
68+
const disposables = new DisposableStore();
69+
const cts = new CancellationTokenSource(token);
70+
const session = Math.random();
71+
72+
this.proxy.$createKeywordRecognitionSession(handle, session);
73+
disposables.add(token.onCancellationRequested(() => this.proxy.$cancelKeywordRecognitionSession(session)));
74+
75+
const onDidChange = disposables.add(new Emitter<IKeywordRecognitionEvent>());
76+
this.keywordRecognitionSessions.set(session, { onDidChange });
77+
78+
return {
79+
onDidChange: onDidChange.event,
80+
dispose: () => {
81+
cts.dispose(true);
82+
this.keywordRecognitionSessions.delete(session);
5783
disposables.dispose();
5884
}
5985
};
@@ -75,9 +101,12 @@ export class MainThreadSpeech extends Disposable implements MainThreadSpeechShap
75101
}
76102

77103
$emitSpeechToTextEvent(session: number, event: ISpeechToTextEvent): void {
78-
const providerSession = this.providerSessions.get(session);
79-
if (providerSession) {
80-
providerSession.onDidChange.fire(event);
81-
}
104+
const providerSession = this.speechToTextSessions.get(session);
105+
providerSession?.onDidChange.fire(event);
106+
}
107+
108+
$emitKeywordRecognitionEvent(session: number, event: IKeywordRecognitionEvent): void {
109+
const providerSession = this.keywordRecognitionSessions.get(session);
110+
providerSession?.onDidChange.fire(event);
82111
}
83112
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
16271627
StackFrameFocus: extHostTypes.StackFrameFocus,
16281628
ThreadFocus: extHostTypes.ThreadFocus,
16291629
RelatedInformationType: extHostTypes.RelatedInformationType,
1630-
SpeechToTextStatus: extHostTypes.SpeechToTextStatus
1630+
SpeechToTextStatus: extHostTypes.SpeechToTextStatus,
1631+
KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus
16311632
};
16321633
};
16331634
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
6464
import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
6565
import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search';
6666
import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers';
67-
import { ISpeechProviderMetadata, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService';
67+
import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService';
6868
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
6969
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
7070
import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy';
@@ -1159,11 +1159,15 @@ export interface MainThreadSpeechShape extends IDisposable {
11591159
$unregisterProvider(handle: number): void;
11601160

11611161
$emitSpeechToTextEvent(session: number, event: ISpeechToTextEvent): void;
1162+
$emitKeywordRecognitionEvent(session: number, event: IKeywordRecognitionEvent): void;
11621163
}
11631164

11641165
export interface ExtHostSpeechShape {
11651166
$createSpeechToTextSession(handle: number, session: number): Promise<void>;
11661167
$cancelSpeechToTextSession(session: number): Promise<void>;
1168+
1169+
$createKeywordRecognitionSession(handle: number, session: number): Promise<void>;
1170+
$cancelKeywordRecognitionSession(session: number): Promise<void>;
11671171
}
11681172

11691173
export interface MainThreadChatProviderShape extends IDisposable {

src/vs/workbench/api/common/extHostSpeech.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ export class ExtHostSpeech implements ExtHostSpeechShape {
5252
this.sessions.delete(session);
5353
}
5454

55+
async $createKeywordRecognitionSession(handle: number, session: number): Promise<void> {
56+
const provider = this.providers.get(handle);
57+
if (!provider) {
58+
return;
59+
}
60+
61+
const disposables = new DisposableStore();
62+
63+
const cts = new CancellationTokenSource();
64+
this.sessions.set(session, cts);
65+
66+
const keywordRecognitionSession = disposables.add(provider.provideKeywordRecognitionSession(cts.token));
67+
disposables.add(keywordRecognitionSession.onDidChange(e => {
68+
if (cts.token.isCancellationRequested) {
69+
return;
70+
}
71+
72+
this.proxy.$emitKeywordRecognitionEvent(session, e);
73+
}));
74+
75+
disposables.add(cts.token.onCancellationRequested(() => disposables.dispose()));
76+
}
77+
78+
async $cancelKeywordRecognitionSession(session: number): Promise<void> {
79+
this.sessions.get(session)?.dispose(true);
80+
this.sessions.delete(session);
81+
}
82+
5583
registerProvider(extension: ExtensionIdentifier, identifier: string, provider: vscode.SpeechProvider): IDisposable {
5684
const handle = ExtHostSpeech.ID_POOL++;
5785

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4205,4 +4205,9 @@ export enum SpeechToTextStatus {
42054205
Stopped = 4
42064206
}
42074207

4208+
export enum KeywordRecognitionStatus {
4209+
Recognized = 1,
4210+
Stopped = 2
4211+
}
4212+
42084213
//#endregion

src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
7-
import { registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
7+
import { DynamicSpeechAccessibilityConfiguration, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
88
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
99
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
1010
import { Registry } from 'vs/platform/registry/common/platform';
@@ -30,3 +30,4 @@ workbenchContributionsRegistry.registerWorkbenchContribution(NotificationAccessi
3030
workbenchContributionsRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually);
3131
workbenchContributionsRegistry.registerWorkbenchContribution(AccessibilityStatus, LifecyclePhase.Ready);
3232
workbenchContributionsRegistry.registerWorkbenchContribution(SaveAudioCueContribution, LifecyclePhase.Ready);
33+
workbenchContributionsRegistry.registerWorkbenchContribution(DynamicSpeechAccessibilityConfiguration, LifecyclePhase.Ready);

src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { Registry } from 'vs/platform/registry/common/platform';
99
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
1010
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
1111
import { AccessibilityAlertSettingId } from 'vs/platform/audioCues/browser/audioCueService';
12+
import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService';
13+
import { Disposable } from 'vs/base/common/lifecycle';
14+
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
15+
import { Event } from 'vs/base/common/event';
1216

1317
export const accessibilityHelpIsShown = new RawContextKey<boolean>('accessibilityHelpIsShown', false, true);
1418
export const accessibleViewIsShown = new RawContextKey<boolean>('accessibleViewIsShown', false, true);
@@ -35,11 +39,6 @@ export const enum ViewDimUnfocusedOpacityProperties {
3539
Maximum = 1
3640
}
3741

38-
export const enum AccessibilityVoiceSettingId {
39-
SpeechTimeout = 'accessibility.voice.speechTimeout',
40-
}
41-
export const SpeechTimeoutDefault = 1200;
42-
4342
export const enum AccessibilityVerbositySettingId {
4443
Terminal = 'accessibility.verbosity.terminal',
4544
DiffEditor = 'accessibility.verbosity.diffEditor',
@@ -77,10 +76,14 @@ const baseProperty: object = {
7776
tags: ['accessibility']
7877
};
7978

80-
const configuration: IConfigurationNode = {
79+
export const accessibilityConfigurationNodeBase = Object.freeze<IConfigurationNode>({
8180
id: 'accessibility',
8281
title: localize('accessibilityConfigurationTitle', "Accessibility"),
83-
type: 'object',
82+
type: 'object'
83+
});
84+
85+
const configuration: IConfigurationNode = {
86+
...accessibilityConfigurationNodeBase,
8487
properties: {
8588
[AccessibilityVerbositySettingId.Terminal]: {
8689
description: localize('verbosity.terminal.description', 'Provide information about how to access the terminal accessibility help menu when the terminal is focused.'),
@@ -251,13 +254,6 @@ const configuration: IConfigurationNode = {
251254
'default': true,
252255
tags: ['accessibility']
253256
},
254-
[AccessibilityVoiceSettingId.SpeechTimeout]: {
255-
'markdownDescription': localize('voice.speechTimeout', "The duration in milliseconds that voice speech recognition remains active after you stop speaking. For example in a chat session, the transcribed text is submitted automatically after the timeout is met. Set to `0` to disable this feature."),
256-
'type': 'number',
257-
'default': SpeechTimeoutDefault,
258-
'minimum': 0,
259-
'tags': ['accessibility']
260-
},
261257
[AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress]: {
262258
markdownDescription: localize('terminal.integrated.accessibleView.closeOnKeyPress', "On keypress, close the Accessible View and focus the element from which it was invoked."),
263259
type: 'boolean',
@@ -298,3 +294,39 @@ export function registerAccessibilityConfiguration() {
298294
}
299295
});
300296
}
297+
298+
export const enum AccessibilityVoiceSettingId {
299+
SpeechTimeout = 'accessibility.voice.speechTimeout'
300+
}
301+
export const SpeechTimeoutDefault = 1200;
302+
303+
export class DynamicSpeechAccessibilityConfiguration extends Disposable implements IWorkbenchContribution {
304+
305+
constructor(
306+
@ISpeechService private readonly speechService: ISpeechService
307+
) {
308+
super();
309+
310+
this._register(Event.runAndSubscribe(speechService.onDidRegisterSpeechProvider, () => this.updateConfiguration()));
311+
}
312+
313+
private updateConfiguration(): void {
314+
if (!this.speechService.hasSpeechProvider) {
315+
return; // these settings require a speech provider
316+
}
317+
318+
const registry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
319+
registry.registerConfiguration({
320+
...accessibilityConfigurationNodeBase,
321+
properties: {
322+
[AccessibilityVoiceSettingId.SpeechTimeout]: {
323+
'markdownDescription': localize('voice.speechTimeout', "The duration in milliseconds that voice speech recognition remains active after you stop speaking. For example in a chat session, the transcribed text is submitted automatically after the timeout is met. Set to `0` to disable this feature."),
324+
'type': 'number',
325+
'default': SpeechTimeoutDefault,
326+
'minimum': 0,
327+
'tags': ['accessibility']
328+
}
329+
}
330+
});
331+
}
332+
}

0 commit comments

Comments
 (0)