Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should interrupt speech synthesis after microphone button is clicked #2429

Merged
merged 11 commits into from
Sep 30, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fix [#2415](https://github.com/microsoft/BotFramework-WebChat/issues/2415) and [#2416](https://github.com/microsoft/BotFramework-WebChat/issues/2416). Fix receipt card rendering, by [@compulim](https://github.com/compulim) in PR [#2417](https://github.com/microsoft/BotFramework-WebChat/issues/2417)
- Fix [#2415](https://github.com/microsoft/BotFramework-WebChat/issues/2415) and [#2416](https://github.com/microsoft/BotFramework-WebChat/issues/2416). Fix Adaptive Cards cannot be disabled on-the-fly, by [@compulim](https://github.com/compulim) in PR [#2417](https://github.com/microsoft/BotFramework-WebChat/issues/2417)
- Fix [#2360](https://github.com/microsoft/BotFramework-WebChat/issues/2360). Timestamp should update on language change, by [@compulim](https://github.com/compulim) in PR [#2414](https://github.com/microsoft/BotFramework-WebChat/pull/2414)
- Fix [#2428](https://github.com/microsoft/BotFramework-WebChat/issues/2428). Should interrupt speech synthesis after microphone button is clicked, by [@compulim](https://github.com/compulim) in PR [#2429](https://github.com/microsoft/BotFramework-WebChat/pull/2429)

### Added

Expand Down
26 changes: 26 additions & 0 deletions __tests__/speech.synthesis.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { timeouts } from './constants.json';

import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown';
import negateCondition from './setup/conditions/negate';
import speechRecognitionStartCalled from './setup/conditions/speechRecognitionStartCalled';
import speechSynthesisUtterancePended from './setup/conditions/speechSynthesisUtterancePended';

Expand Down Expand Up @@ -98,4 +99,29 @@ describe('speech synthesis', () => {

expect((await pageObjects.getConsoleLogs()).filter(([type]) => type === 'error')).toEqual([]);
});

test('should stop synthesis after clicking on microphone button', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
webSpeechPonyfillFactory: () => window.WebSpeechMock
}
});

await pageObjects.sendMessageViaMicrophone('echo Hello, World!');

await expect(speechRecognitionStartCalled().fn(driver)).resolves.toBeFalsy();
await driver.wait(minNumActivitiesShown(2), timeouts.directLine);

await expect(pageObjects.startSpeechSynthesize()).resolves.toHaveProperty(
'text',
'Echoing back in a separate activity.'
);

await driver.wait(speechSynthesisUtterancePended(), timeouts.ui);

await pageObjects.clickMicrophoneButton();

await expect(speechRecognitionStartCalled().fn(driver)).resolves.toBeTruthy();
await driver.wait(negateCondition(speechSynthesisUtterancePended()), timeouts.ui);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import speakActivityAndStartDictateOnIncomingActivityFromOthersSaga from './saga
import startSpeakActivityOnPostActivitySaga from './sagas/startSpeakActivityOnPostActivitySaga';
import stopDictateOnCardActionSaga from './sagas/stopDictateOnCardActionSaga';
import stopSpeakingActivityOnInputSaga from './sagas/stopSpeakingActivityOnInputSaga';
import stopSpeakingActivityOnStartDictate from './sagas/stopSpeakingActivityOnStartDictate';
import submitSendBoxSaga from './sagas/submitSendBoxSaga';

export default function* sagas() {
Expand All @@ -41,5 +42,6 @@ export default function* sagas() {
yield fork(startSpeakActivityOnPostActivitySaga);
yield fork(stopDictateOnCardActionSaga);
yield fork(stopSpeakingActivityOnInputSaga);
yield fork(stopSpeakingActivityOnStartDictate);
yield fork(submitSendBoxSaga);
}
15 changes: 15 additions & 0 deletions packages/core/src/sagas/stopSpeakingActivityOnStartDictate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { put, takeEvery } from 'redux-saga/effects';

import { START_DICTATE } from '../actions/startDictate';
import stopSpeakingActivity from '../actions/stopSpeakingActivity';
import whileConnected from './effects/whileConnected';

function* stopSpeakingActivityOnStartDictate() {
yield takeEvery(START_DICTATE, function*() {
yield put(stopSpeakingActivity());
});
}

export default function* stopSpeakingActivityOnStartDictateSaga() {
yield whileConnected(stopSpeakingActivityOnStartDictate);
}
12 changes: 6 additions & 6 deletions packages/embed/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 61 additions & 89 deletions samples/06.c.cognitive-services-speech-services-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,100 +34,72 @@ Cognitive Services Speech Services has published a new API to provide speech rec

## Completed code

#### Using subscription key

> This approach is for demonstration purposes only. In production code, you should always store the subscription key on a secured token server. The token server should only send out limited authorization code. This [article on authorizations](https://docs.microsoft.com/en-us/azure/cognitive-services/speech/api-reference-rest/websocketprotocol#authorization) outlines the authorization process.

In this portion, we are hardcoding the subscription key in the client code.

Here is the finished `index.html` for subscription key flow:
### Using authorization token

```diff
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Web Chat: Cognitive Services Speech Services using JavaScript</title>

<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<style>
html, body { height: 100% }
body { margin: 0 }

#webchat {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="webchat" role="main"></div>
<script>
(async function () {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

+ const searchParams = new URLSearchParams(window.location.search);

+ const subscriptionKey = searchParams.get('s');

+ const webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
+ region: searchParams.get('r') || 'westus',
+ subscriptionKey
+ });
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Web Chat: Cognitive Services Speech Services using JavaScript</title>
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<style>
html, body { height: 100% }
body { margin: 0 }

#webchat {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="webchat" role="main"></div>
<script>
+ function createFetchSpeechServicesCredentials() {
+ let expireAfter = 0;
+ let lastResult = {};
+
+ return async () => {
+ if (Date.now() > expireAfter) {
+ const speechServicesTokenRes = await fetch('https://webchat-mockbot.azurewebsites.net/speechservices/token', { method: 'POST' });
+
+ lastResult = await speechServicesTokenRes.json();
+ expireAfter = Date.now() + 300000;
+ }
+
+ return lastResult;
+ }
+ }
+
+ const fetchSpeechServicesCredentials = createFetchSpeechServicesCredentials();
+
+ async function fetchSpeechServicesRegion() {
+ return (await fetchSpeechServicesCredentials()).region;
+ }
+
+ async function fetchSpeechServicesToken() {
+ return (await fetchSpeechServicesCredentials()).token;
+ }

(async function () {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

+ const webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
+ authorizationToken: fetchSpeechServicesToken,
+ region: await fetchSpeechServicesRegion()
+ });

window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
+ webSpeechPonyfillFactory
}, document.getElementById('webchat'));

document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
</script>
</body>
</html>
```

#### Using authorization token
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
+ webSpeechPonyfillFactory
}, document.getElementById('webchat'));

```diff
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Web Chat: Cognitive Services Speech Services using JavaScript</title>
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<style>
html, body { height: 100% }
body { margin: 0 }

#webchat {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="webchat" role="main"></div>
<script>
(async function () {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

+ const res = await fetch('https://webchat-mockbot.azurewebsites.net/speechservices/token', { method: 'POST' });
+ const { region, token: authorizationToken } = await res.json();

+ const webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({ authorizationToken, region });

window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
+ webSpeechPonyfillFactory
}, document.getElementById('webchat'));

document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
</script>
</body>
</html>
document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
</script>
</body>
</html>
```

# Further reading
Expand Down
65 changes: 44 additions & 21 deletions samples/06.c.cognitive-services-speech-services-js/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,59 @@
<body>
<div id="webchat" role="main"></div>
<script>
// Create a function to fetch the Cognitive Services Speech Services credentials.
// The async function created will hold expiration information about the token and will return cached token when possible.
function createFetchSpeechServicesCredentials() {
let expireAfter = 0;
let lastResult = {};

return async () => {
// Fetch a new token if the existing is expiring.
// The following article mentioned the token is only valid for 10 minutes.
// We will invalidate the token after 5 minutes.
// https://docs.microsoft.com/en-us/azure/cognitive-services/authentication#authenticate-with-an-authentication-token
if (Date.now() > expireAfter) {
const speechServicesTokenRes = await fetch(
'https://webchat-mockbot.azurewebsites.net/speechservices/token',
{ method: 'POST' }
);

lastResult = await speechServicesTokenRes.json();
expireAfter = Date.now() + 300000;
}

return lastResult;
};
}

const fetchSpeechServicesCredentials = createFetchSpeechServicesCredentials();

async function fetchSpeechServicesRegion() {
return (await fetchSpeechServicesCredentials()).region;
}

async function fetchSpeechServicesToken() {
return (await fetchSpeechServicesCredentials()).token;
}

(async function() {
// In this demo, we are using Direct Line token from MockBot.
// Your client code must provide either a secret or a token to talk to your bot.
// Tokens are more secure. To learn about the differences between secrets and tokens
// Tokens are more secure. To learn about the differences between secrets and tokens.
// and to understand the risks associated with using secrets, visit https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication?view=azure-bot-service-4.0

const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

// To run this demo, you can either use our authorization token, or provide your own subscription key thru ?s=BING_SPEECH_SUBSCRIPTION_KEY
const searchParams = new URLSearchParams(window.location.search);
const subscriptionKey = searchParams.get('s');
let webSpeechPonyfillFactory;

if (subscriptionKey) {
// In case you are using your own subscription key, please note that client should always authenticate against your server
// to avoid exposing the subscription key to any part of your client code.
webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
region: searchParams.get('r') || 'westus',
subscriptionKey
});
} else {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/speechservices/token', { method: 'POST' });
const { region, token: authorizationToken } = await res.json();
// Create the ponyfill factory function, that can be called to create a concrete implementation of the ponyfill.
compulim marked this conversation as resolved.
Show resolved Hide resolved
const webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
// We are passing the Promise function to the authorizationToken field.
// This function will be called every time the token is being used.
authorizationToken: fetchSpeechServicesToken,

webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
authorizationToken,
region
});
}
// In contrast, we are only fetching the region once.
region: await fetchSpeechServicesRegion()
});

// Pass a Web Speech ponyfill factory to renderWebChat.
// You can also use your own speech engine given it is compliant to W3C Web Speech API, https://w3c.github.io/speech-api/.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,15 @@ We'll start by using the [Cognitive Services Speech Services sample](./../06.c.c

The main change you will need to make, regardless of whether you are using the subscription key or authorization token, is adding the value `'lexical'` to a `textNormalization` key in your `webSpeechPonyFillFactory` object.

Subscription key:

```diff
webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
region: searchParams.get('r') || 'westus',
subscriptionKey,
+ textNormalization: 'lexical'
});
```

Authorization Token:

```diff
const res = await fetch('https://webchat-mockbot.azurewebsites.net/speechservices/token', { method: 'POST' });
const { region, token: authorizationToken } = await res.json();

webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
authorizationToken,
region,
+ textNormalization: 'lexical'
});
const res = await fetch('https://webchat-mockbot.azurewebsites.net/speechservices/token', { method: 'POST' });
const { region, token: authorizationToken } = await res.json();

webSpeechPonyfillFactory = await window.WebChat.createCognitiveServicesSpeechServicesPonyfillFactory({
+ authorizationToken: fetchSpeechServicesToken,
+ region: await fetchSpeechServicesRegion()
+ textNormalization: 'lexical'
});
```

# Further reading
Expand Down
Loading