Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
compulim committed Aug 19, 2019
1 parent 3e353ec commit cbe0b32
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `playground`: Remove [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) from `dependencies`
- `samples/*`: Move to production version of Web Chat, and bump to [`react@16.8.6`](https://www.npmjs.com/package/react) and [`react-dom@16.8.6`](https://www.npmjs.com/package/react-dom)
- Moved the typing indicator to the send box and removed the typing indicator logic from the sagas, by [@tdurnford](https://github.com/tdurnford), in PR [#2321](https://github.com/microsoft/BotFramework-WebChat/pull/2321)
- Added function to select voice to props, `selectVoice()`, by [@compulim](https://github.com/compulim), in PR [#2292](https://github.com/microsoft/BotFramework-WebChat/pull/2292)

### Fixed

Expand Down
14 changes: 2 additions & 12 deletions packages/component/src/Activity/Speak.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,10 @@ import SayAlt from './SayAlt';
// TODO: [P3] We should add a "spoken" or "speakState" flag to indicate whether this activity is going to speak, or spoken
const connectSpeakActivity = (...selectors) =>
connectToWebChat(
({ language, markActivity }, { activity }) => ({
({ language, markActivity, selectVoice }, { activity }) => ({
language,
markAsSpoken: () => markActivity(activity, 'speak', false),
selectVoice: voices => {
voices = [].slice.call(voices);

return (
voices.find(({ lang }) => lang === activity.locale) ||
voices.find(({ lang }) => lang === language) ||
voices.find(({ lang }) => lang === window.navigator.language) ||
voices.find(({ lang }) => lang === 'en-US') ||
voices[0]
);
}
selectVoice: voices => selectVoice(voices, { language }, activity)
}),
...selectors
);
Expand Down
3 changes: 3 additions & 0 deletions packages/component/src/Composer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import concatMiddleware from './Middleware/concatMiddleware';
import Context from './Context';
import createCoreCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
import createStyleSet from './Styles/createStyleSet';
import defaultSelectVoice from './defaultSelectVoice';
import Dictation from './Dictation';
import mapMap from './Utils/mapMap';
import observableToPromise from './Utils/observableToPromise';
Expand Down Expand Up @@ -351,6 +352,7 @@ Composer.defaultProps = {
locale: window.navigator.language || 'en-US',
referenceGrammarID: '',
renderMarkdown: text => text,
selectVoice: defaultSelectVoice,
sendTimeout: 20000,
sendTyping: undefined,
sendTypingIndicator: false,
Expand Down Expand Up @@ -388,6 +390,7 @@ Composer.propTypes = {
referenceGrammarID: PropTypes.string,
renderMarkdown: PropTypes.func,
scrollToEnd: PropTypes.func.isRequired,
selectVoice: PropTypes.func,
sendTimeout: PropTypes.number,
sendTyping: PropTypes.bool,
sendTypingIndicator: PropTypes.bool,
Expand Down
19 changes: 19 additions & 0 deletions packages/component/src/defaultSelectVoice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default function(voices, { language }, activity) {
// Find the first voice based on this order:
// 1. Voice with language same as locale as defined in the activity
// 2. Voice with language same as locale as passed into Web Chat
// 3. Voice with language same as browser
// 4. English (United States)
// 5. First voice

// We also prefer voice powered by deep neural network (with keyword "neural" in the voice name).
return (
[activity.locale, language, window.navigator.language, 'en-US'].reduce(
(result, targetLanguage) =>
result ||
voices.find(({ lang, name }) => lang === targetLanguage && /neural/iu.test(name)) ||
voices.find(({ lang }) => lang === targetLanguage),
null
) || voices[0]
);
}
50 changes: 47 additions & 3 deletions packages/playground/src/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from 'glamor';
import React from 'react';
import memoize from 'memoize-one';
import React from 'react';

import ReactWebChat, {
createBrowserWebSpeechPonyfillFactory,
Expand Down Expand Up @@ -71,7 +71,9 @@ export default class extends React.Component {
this.handleUseEmulatorCoreClick = this.handleUseEmulatorCoreClick.bind(this);
this.handleUseMockBot = this.handleUseMockBot.bind(this);
this.handleUserAvatarInitialsChange = this.handleUserAvatarInitialsChange.bind(this);
this.handleVoiceGenderPreferenceChange = this.handleVoiceGenderPreferenceChange.bind(this);
this.handleWordBreakChange = this.handleWordBreakChange.bind(this);
this.selectVoiceWithGender = this.selectVoiceWithGender.bind(this);

this.mainRef = React.createRef();
this.activityMiddleware = createDevModeActivityMiddleware();
Expand Down Expand Up @@ -154,6 +156,7 @@ export default class extends React.Component {
userAvatarInitials: 'WC',
userID,
username: 'Web Chat user',
voiceGenderPreference: window.sessionStorage.getItem('PLAYGROUND_VOICE_GENDER_PREFERENCE') || false,
webSpeechPonyfillFactory: undefined,
workBreak: ''
};
Expand Down Expand Up @@ -195,7 +198,7 @@ export default class extends React.Component {

const webSpeechPonyfillFactory = createCognitiveServicesSpeechServicesPonyfillFactory({
authorizationToken: () => fetchAuthorizationToken(Date.now()),
region: 'westus'
region: 'westus2'
});

this.setState(() => ({ webSpeechPonyfillFactory }));
Expand Down Expand Up @@ -249,6 +252,15 @@ export default class extends React.Component {
);
}

handleVoiceGenderPreferenceChange({ target: { value } }) {
this.setState(
() => ({ voiceGenderPreference: value || null }),
() => {
window.sessionStorage.setItem('PLAYGROUND_VOICE_GENDER_PREFERENCE', value);
}
);
}

handleReliabilityChange({ target: { checked } }) {
this.setState(() => ({ faulty: !checked }), () => this.state.directLine.setFaulty(this.state.faulty));
}
Expand Down Expand Up @@ -314,6 +326,25 @@ export default class extends React.Component {
this.setState(() => ({ wordBreak: value }));
}

selectVoiceWithGender(voices, { language }, activity) {
const { voiceGenderPreference } = this.state;

return (
[activity.locale, language, window.navigator.language, 'en-US'].reduce(
(result, targetLanguage) =>
result ||
voices.find(
({ gender, lang, name }) =>
(gender || '').toLowerCase() === voiceGenderPreference && lang === targetLanguage && /neural/iu.test(name)
) ||
voices.find(
({ gender, lang }) => (gender || '').toLowerCase() === voiceGenderPreference && lang === targetLanguage
),
null
) || voices[0]
);
}

render() {
const {
props: { store },
Expand All @@ -333,10 +364,12 @@ export default class extends React.Component {
userAvatarInitials,
userID,
username,
voiceGenderPreference,
webSpeechPonyfillFactory,
wordBreak
}
} = this;

const styleOptions = this.createMemoizedStyleOptions(
hideSendBox,
botAvatarInitials,
Expand All @@ -359,6 +392,7 @@ export default class extends React.Component {
directLine={directLine}
disabled={disabled}
locale={language}
selectVoice={voiceGenderPreference ? this.selectVoiceWithGender : undefined}
sendTimeout={+sendTimeout || undefined}
sendTypingIndicator={sendTypingIndicator}
store={store}
Expand Down Expand Up @@ -513,7 +547,7 @@ export default class extends React.Component {
<label>
Word break
<select onChange={this.handleWordBreakChange} value={wordBreak || 'break-word'}>
<option value="break-word">Break word </option>
<option value="break-word">Break word</option>
<option value="normal">Normal</option>
<option value="break-all">Break all</option>
<option value="keep-all">Keep all</option>
Expand All @@ -530,6 +564,16 @@ export default class extends React.Component {
Rich card wrap title
</label>
</div>
<div>
<label>
Voice gender preference
<select onChange={this.handleVoiceGenderPreferenceChange} value={voiceGenderPreference || ''}>
<option value="">No preferences</option>
<option value="female">Female</option>
<option value="male">Male</option>
</select>
</label>
</div>
</div>
</div>
);
Expand Down
43 changes: 43 additions & 0 deletions samples/06.g.select-voice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Sample - Integrating with Cognitive Services Speech Services

## Description

A simple page with Web Chat integrated with speech-to-text and text-to-speech functionality and select different voices.

# Test out the hosted sample

- [Try out MockBot](https://microsoft.github.io/BotFramework-WebChat/06.g.select-voice)

# How to run

- Fork this repository
- Navigate to `/Your-Local-WebChat/samples/06.g.select-voice` in command line
- Run `npx serve`
- Browse to [http://localhost:5000/](http://localhost:5000/)

# Things to try out

- Click on microphone button
- Say "Tell me a story."
- It should recognize as "Tell me a story."
- It should speak out two activities sent from the bot. One in English, another Cantonese.

# Code

> Jump to [completed code](#completed-code) to see the end-result `index.html`.
### Goals of this bot

Demonstrates ability to select different voice for speech synthesis on-the-fly.

## Completed code

(TBD)

# Further reading

- [Cognitive Speech Speech Services website](https://azure.microsoft.com/en-us/services/cognitive-services/speech-services/)

## Full list of Web Chat Hosted Samples

View the list of [available Web Chat samples](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples)
57 changes: 57 additions & 0 deletions samples/06.g.select-voice/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Web Chat: Select voice</title>
<!-- Cognitive Services Speech Services adapter is only available in full bundle -->

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
This CDN points to the latest official release of Web Chat. If you need to test against Web Chat's latest bits, please refer to pointing to Web Chat's MyGet feed:
https://github.com/microsoft/BotFramework-WebChat#how-to-test-with-web-chats-latest-bits
-->
<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 () {
// 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
// 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 directLineTokenResponse = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await directLineTokenResponse.json();

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

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

window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
selectVoice: (voices, { language }, activity) =>
// If the activity is in Cantonese, use a voice with keyword "TracyRUS".
// Otherwise, use "JessaNeural" (preferred) or "Jessa".
activity.locale === 'zh-HK' ?
voices.find(({ name }) => /TracyRUS/iu.test(name))
:
voices.find(({ name }) => /JessaNeural/iu.test(name))
|| voices.find(({ name }) => /Jessa/iu.test(name)),
webSpeechPonyfillFactory
}, document.getElementById('webchat'));

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

0 comments on commit cbe0b32

Please sign in to comment.