Skip to content
Snippets Groups Projects
Commit 9e057b9a authored by Pedro Beschorner Marin's avatar Pedro Beschorner Marin
Browse files

Refactoring closed captions speech recognizer

parent a5f75798
No related branches found
No related tags found
No related merge requests found
......@@ -8,23 +8,23 @@ import {
appendTextURL,
} from '/imports/api/note/server/helpers';
export default function appendText(body, textData, locale) {
export default function appendText(body, text, locale) {
const { meetingId } = body;
check(meetingId, String);
check(textData, String);
check(text, String);
check(locale, String);
const padId = generatePadId(meetingId, locale);
axios({
method: 'get',
url: appendTextURL(padId, textData),
url: appendTextURL(padId, text),
responseType: 'json',
}).then((response) => {
const { status } = response;
if (status === 200) {
Logger.info(`Appended text for padId:${padId}`);
Logger.verbose(`Appended text for padId:${padId}`);
}
}).catch(error => Logger.error(`Could not append captions for padId=${padId}: ${error}`));
}
......@@ -4,6 +4,7 @@ import { Session } from 'meteor/session';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/button/component';
import logger from '/imports/startup/client/logger';
import PadService from './service';
import CaptionsService from '/imports/ui/components/captions/service';
import { styles } from './styles';
......@@ -21,6 +22,10 @@ const intlMessages = defineMessages({
id: 'app.captions.pad.ownership',
description: 'Label for taking ownership of closed captions pad',
},
interimResult: {
id: 'app.captions.pad.interimResult',
description: 'Title for speech recognition interim results',
},
dictationStart: {
id: 'app.captions.pad.dictationStart',
description: 'Label for starting speech recognition',
......@@ -47,16 +52,11 @@ const propTypes = {
readOnlyPadId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
amIModerator: PropTypes.bool.isRequired,
handleAppendText: PropTypes.func.isRequired,
initVoiceRecognition: PropTypes.func.isRequired,
formatEntry: PropTypes.func.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
};
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
class Pad extends PureComponent {
static getDerivedStateFromProps(nextProps) {
if (nextProps.ownerId !== nextProps.currentUserId) {
......@@ -72,8 +72,8 @@ class Pad extends PureComponent {
listening: false,
};
const { initVoiceRecognition } = props;
this.recognition = initVoiceRecognition();
const { locale } = props;
this.recognition = CaptionsService.initSpeechRecognition(locale);
this.toggleListen = this.toggleListen.bind(this);
this.handleListen = this.handleListen.bind(this);
......@@ -107,16 +107,14 @@ class Pad extends PureComponent {
listening,
} = this.state;
const {
formatEntry,
handleAppendText,
} = this.props;
if (this.recognition) {
// Starts and stops the recognition when listening.
// Throws an error if start() is called on a recognition that has already been started.
if (listening) this.recognition.start();
if (!listening) this.recognition.stop();
if (listening) {
this.recognition.start();
} else {
this.recognition.stop();
}
// Stores the voice recognition results that have been verified.
let finalTranscript = '';
......@@ -138,24 +136,24 @@ class Pad extends PureComponent {
else interimTranscript += transcript;
}
// Adds the interimTranscript text to the itermResultContainer to show
// Adds the interimTranscript text to the iterimResultContainer to show
// what's being said while speaking.
if (this.itermResultContainer) {
this.itermResultContainer.innerHTML = interimTranscript;
if (this.iterimResultContainer) {
this.iterimResultContainer.innerHTML = interimTranscript;
}
const newEntry = finalTranscript !== '';
// Changes to the finalTranscript are shown to in the captions
if (newEntry) {
const formattedTranscript = formatEntry(finalTranscript.trimRight());
handleAppendText(formattedTranscript);
const text = finalTranscript.trimRight();
CaptionsService.appendText(text);
finalTranscript = '';
}
};
this.recognition.onerror = (event) => {
console.log(`Error occurred in recognition: ${event.error}`);
logger.error({ logCode: 'captions_recognition' }, event.error);
};
}
}
......@@ -169,7 +167,6 @@ class Pad extends PureComponent {
ownerId,
name,
amIModerator,
currentUserId,
} = this.props;
if (!amIModerator) {
......@@ -178,8 +175,6 @@ class Pad extends PureComponent {
}
const { listening } = this.state;
const { enableDictation } = CAPTIONS_CONFIG;
const allowDictation = enableDictation && currentUserId === ownerId;
const url = PadService.getPadURL(padId, readOnlyPadId, ownerId);
return (
......@@ -194,7 +189,7 @@ class Pad extends PureComponent {
className={styles.hideBtn}
/>
</div>
{allowDictation
{CaptionsService.canIDictateThisPad(ownerId)
? (
<span>
<Button
......@@ -233,10 +228,12 @@ class Pad extends PureComponent {
</header>
{listening ? (
<div>
<span className={styles.intermTitle}>Interm results</span>
<span className={styles.interimTitle}>
{intl.formatMessage(intlMessages.interimResult)}
</span>
<div
className={styles.processing}
ref={(node) => { this.itermResultContainer = node; }}
ref={(node) => { this.iterimResultContainer = node; }}
/>
</div>
) : null
......
import React, { PureComponent } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import { makeCall } from '/imports/ui/services/api';
import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import CaptionsService from '/imports/ui/components/captions/service';
import PadService from './service';
import Pad from './component';
class PadContainer extends PureComponent {
......@@ -29,39 +27,13 @@ export default withTracker(() => {
const { name } = caption ? caption.locale : '';
const handleAppendText = text => makeCall('appendText', text, locale);
const initVoiceRecognition = () => {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition = null;
if (SR) {
recognition = new SR();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = locale;
}
return recognition;
};
const formatEntry = (string) => {
const letterIndex = string.charAt(0) === ' ' ? 1 : 0;
const formattedString = `${string.charAt(letterIndex).toUpperCase() + string.slice(letterIndex + 1)}.\n\n`;
return formattedString;
};
const currentUserId = Users.findOne({ userId: Auth.userID }).userId;
return {
locale,
name,
ownerId,
padId,
readOnlyPadId,
currentUserId: PadService.getCurrentUser().userId,
amIModerator: CaptionsService.amIModerator(),
handleAppendText,
initVoiceRecognition,
currentUserId,
formatEntry,
};
})(PadContainer);
......@@ -43,4 +43,5 @@ const getPadURL = (padId, readOnlyPadId, ownerId) => {
export default {
getPadURL,
getCurrentUser,
};
......@@ -13,7 +13,7 @@
border: solid var(--border-size-small) var(--color-gray);
}
.intermTitle {
.interimTitle {
margin-top: var(--lg-padding-y);
}
......
......@@ -68,6 +68,16 @@ const takeOwnership = (locale) => {
makeCall('takeOwnership', locale);
};
const formatEntry = (entry) => {
const letterIndex = entry.charAt(0) === ' ' ? 1 : 0;
const formattedEntry = `${entry.charAt(letterIndex).toUpperCase() + entry.slice(letterIndex + 1)}.\n\n`;
return formattedEntry;
};
const appendText = (text, locale) => {
makeCall('appendText', formatEntry(text), locale);
};
const canIOwnThisPad = (ownerId) => {
const { userID } = Auth;
if (!CAPTIONS_CONFIG.takeOwnership) return false;
......@@ -75,6 +85,15 @@ const canIOwnThisPad = (ownerId) => {
return ownerId !== userID;
};
const canIDictateThisPad = (ownerId) => {
const { userID } = Auth;
if (!CAPTIONS_CONFIG.enableDictation) return false;
if (ownerId === '') return false;
const SpeechRecognitionAPI = getSpeechRecognitionAPI();
if (!SpeechRecognitionAPI) return false;
return ownerId === userID;
};
const setActiveCaptions = (locale) => {
Session.set('activeCaptions', locale);
};
......@@ -137,13 +156,32 @@ const amIModerator = () => {
return mapUser(currentUser).isModerator;
};
const getSpeechRecognitionAPI = () => {
return window.SpeechRecognition || window.webkitSpeechRecognition;
};
const initSpeechRecognition = (locale) => {
const SpeechRecognitionAPI = getSpeechRecognitionAPI();
let recognition = null;
if (SpeechRecognitionAPI) {
recognition = new SpeechRecognitionAPI();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = locale;
}
return recognition;
};
export default {
getCaptionsData,
getAvailableLocales,
getOwnedLocales,
takeOwnership,
appendText,
getCaptions,
canIOwnThisPad,
canIDictateThisPad,
getCaptionsSettings,
isCaptionsEnabled,
isCaptionsAvailable,
......@@ -152,4 +190,5 @@ export default {
activateCaptions,
formatCaptionsText,
amIModerator,
initSpeechRecognition,
};
......@@ -36,6 +36,7 @@
"app.captions.pad.hide": "Hide closed captions",
"app.captions.pad.tip": "Press Esc to focus editor toolbar",
"app.captions.pad.ownership": "Take ownership",
"app.captions.pad.interimResult": "Interim results",
"app.captions.pad.dictationStart": "Start Dictation",
"app.captions.pad.dictationStop": "Stop Dictation",
"app.captions.pad.dictationOnDesc": "Turns speech recognition on",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment