ソースを参照

Show subtitles when Jigasi sends transcription results in JSON (#1914)

* Show subtitles when Jigasi sends transcription results in JSON

* fix: Import PropTypes from prop-types.

* apply feedback on initial PR

* Changed Object to Map, alphabetic ordering fixes ,css changes in transcription subtitles

* Sends Map of transcriptMessages as prop to Component

* Documentation fixes and uses config in redux state

* Minor doc fix

* rename feature 'transcription' to 'subtitles'

* Moves subtitles config to interfaceConfig and minor fixes

* minor lint fix
j8
Nik 6年前
コミット
d3dd54ac3b

+ 5
- 0
conference.js ファイルの表示

@@ -109,6 +109,7 @@ import {
109 109
 } from './react/features/overlay';
110 110
 import { setSharedVideoStatus } from './react/features/shared-video';
111 111
 import { isButtonEnabled } from './react/features/toolbox';
112
+import { endpointMessageReceived } from './react/features/subtitles';
112 113
 
113 114
 const logger = require('jitsi-meet-logger').getLogger(__filename);
114 115
 
@@ -1875,6 +1876,10 @@ export default {
1875 1876
             }
1876 1877
         );
1877 1878
 
1879
+        room.on(
1880
+            JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
1881
+            (...args) => APP.store.dispatch(endpointMessageReceived(...args)));
1882
+
1878 1883
         room.on(
1879 1884
             JitsiConferenceEvents.LOCK_STATE_CHANGED,
1880 1885
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));

+ 0
- 1
config.js ファイルの表示

@@ -252,7 +252,6 @@ var config = {
252 252
     // maintenance at 01:00 AM GMT,
253 253
     // noticeMessage: '',
254 254
 
255
-
256 255
     // Stats
257 256
     //
258 257
 

+ 13
- 0
css/_transcription-subtitles.scss ファイルの表示

@@ -0,0 +1,13 @@
1
+.transcription-subtitles{
2
+    bottom: 10%;
3
+    font-size: 16px;
4
+    font-weight: 1000;
5
+    opacity: 0.80;
6
+    position: absolute;
7
+    text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
8
+    0px 1px 1px rgba(0,0,0,0.3),
9
+    1px 0px 1px rgba(0,0,0,0.3),
10
+    0px 0px 1px rgba(0,0,0,0.3);
11
+    width: 100%;
12
+    z-index: $zindex2;
13
+}

+ 1
- 1
css/main.scss ファイルの表示

@@ -77,5 +77,5 @@
77 77
 @import 'unsupported-browser/main';
78 78
 @import 'modals/invite/add-people';
79 79
 @import 'deep-linking/main';
80
-
80
+@import 'transcription-subtitles';
81 81
 /* Modules END */

+ 8
- 0
interface_config.js ファイルの表示

@@ -80,6 +80,14 @@ var interfaceConfig = {
80 80
     DISABLE_FOCUS_INDICATOR: false,
81 81
     DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
82 82
 
83
+    /**
84
+     * Whether the speech to text transcription subtitles panel is disabled.
85
+     * If {@code undefined}, defaults to {@code false}.
86
+     *
87
+     * @type {boolean}
88
+     */
89
+    DISABLE_TRANSCRIPTION_SUBTITLES: false,
90
+
83 91
     /**
84 92
      * Whether the ringing sound in the call/ring overlay is disabled. If
85 93
      * {@code undefined}, defaults to {@code false}.

+ 0
- 1
react/features/base/conference/actions.js ファイルの表示

@@ -54,7 +54,6 @@ import {
54 54
     getCurrentConference,
55 55
     sendLocalParticipant
56 56
 } from './functions';
57
-
58 57
 import type { Dispatch } from 'redux';
59 58
 
60 59
 const logger = require('jitsi-meet-logger').getLogger(__filename);

+ 3
- 0
react/features/large-video/components/LargeVideo.web.js ファイルの表示

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 
6 6
 import { Watermarks } from '../../base/react';
7
+import { TranscriptionSubtitles } from '../../subtitles/';
7 8
 
8 9
 import Labels from './Labels';
9 10
 
@@ -70,6 +71,8 @@ export default class LargeVideo extends Component<*> {
70 71
                             muted = { true } />
71 72
                     </div>
72 73
                 </div>
74
+                { interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
75
+                    ? null : <TranscriptionSubtitles /> }
73 76
                 <span id = 'localConnectionMessage' />
74 77
                 { this.props.hideVideoQualityLabel
75 78
                     ? null : <Labels /> }

+ 46
- 0
react/features/subtitles/actionTypes.js ファイルの表示

@@ -0,0 +1,46 @@
1
+/**
2
+ * The type of (redux) action which indicates that a transcript with
3
+ * a new message_id is received.
4
+ *
5
+ * {
6
+ *      type: ADD_TRANSCRIPT_MESSAGE,
7
+ *      transcriptMessageID: string,
8
+ *      participantName: string
9
+ * }
10
+ */
11
+export const ADD_TRANSCRIPT_MESSAGE = Symbol('ADD_TRANSCRIPT_MESSAGE');
12
+
13
+/**
14
+ * The type of (redux) action which indicates that an endpoint message
15
+ * sent by another participant to the data channel is received.
16
+ *
17
+ * {
18
+ *     type: ENDPOINT_MESSAGE_RECEIVED,
19
+ *     participant: Object,
20
+ *     json: Object
21
+ * }
22
+ */
23
+export const ENDPOINT_MESSAGE_RECEIVED = Symbol('ENDPOINT_MESSAGE_RECEIVED');
24
+
25
+/**
26
+ * The type of (redux) action which indicates that an existing transcript
27
+ * has to be removed from the state.
28
+ *
29
+ * {
30
+ *      type: REMOVE_TRANSCRIPT_MESSAGE,
31
+ *      transciptMessageID: string,
32
+ * }
33
+ */
34
+export const REMOVE_TRANSCRIPT_MESSAGE = Symbol('REMOVE_TRANSCRIPT_MESSAGE');
35
+
36
+/**
37
+ * The type of (redux) action which indicates that a transcript with an
38
+ * existing message_id to be updated is received.
39
+ *
40
+ * {
41
+ *      type: UPDATE_TRANSCRIPT_MESSAGE,
42
+ *      transcriptMessageID: string,
43
+ *      newTranscriptMessage: Object
44
+ * }
45
+ */
46
+export const UPDATE_TRANSCRIPT_MESSAGE = Symbol('UPDATE_TRANSCRIPT_MESSAGE');

+ 84
- 0
react/features/subtitles/actions.js ファイルの表示

@@ -0,0 +1,84 @@
1
+// @flow
2
+
3
+import {
4
+    ADD_TRANSCRIPT_MESSAGE,
5
+    ENDPOINT_MESSAGE_RECEIVED,
6
+    REMOVE_TRANSCRIPT_MESSAGE,
7
+    UPDATE_TRANSCRIPT_MESSAGE
8
+} from './actionTypes';
9
+
10
+/**
11
+ * Signals that a transcript with a new message_id is received.
12
+ *
13
+ * @param {string} transcriptMessageID - The new message_id.
14
+ * @param {string} participantName - The participant name of the sender.
15
+ * @returns {{
16
+ *      type: ADD_TRANSCRIPT_MESSAGE,
17
+ *      transcriptMessageID: string,
18
+ *      participantName: string
19
+ * }}
20
+ */
21
+export function addTranscriptMessage(transcriptMessageID: string,
22
+        participantName: string) {
23
+    return {
24
+        type: ADD_TRANSCRIPT_MESSAGE,
25
+        transcriptMessageID,
26
+        participantName
27
+    };
28
+}
29
+
30
+/**
31
+ * Signals that a participant sent an endpoint message on the data channel.
32
+ *
33
+ * @param {Object} participant - The participant details sending the message.
34
+ * @param {Object} json - The json carried by the endpoint message.
35
+ * @returns {{
36
+ *      type: ENDPOINT_MESSAGE_RECEIVED,
37
+ *      participant: Object,
38
+ *      json: Object
39
+ * }}
40
+ */
41
+export function endpointMessageReceived(participant: Object, json: Object) {
42
+    return {
43
+        type: ENDPOINT_MESSAGE_RECEIVED,
44
+        participant,
45
+        json
46
+    };
47
+}
48
+
49
+/**
50
+ * Signals that a transcript has to be removed from the state.
51
+ *
52
+ * @param {string} transcriptMessageID - The message_id to be removed.
53
+ * @returns {{
54
+ *      type: REMOVE_TRANSCRIPT_MESSAGE,
55
+ *      transcriptMessageID: string,
56
+ * }}
57
+ */
58
+export function removeTranscriptMessage(transcriptMessageID: string) {
59
+    return {
60
+        type: REMOVE_TRANSCRIPT_MESSAGE,
61
+        transcriptMessageID
62
+    };
63
+}
64
+
65
+/**
66
+ * Signals that a transcript with an existing message_id to be updated
67
+ * is received.
68
+ *
69
+ * @param {string} transcriptMessageID -The transcript message_id to be updated.
70
+ * @param {Object} newTranscriptMessage - The updated transcript message.
71
+ * @returns {{
72
+ *      type: UPDATE_TRANSCRIPT_MESSAGE,
73
+ *      transcriptMessageID: string,
74
+ *      newTranscriptMessage: Object
75
+ * }}
76
+ */
77
+export function updateTranscriptMessage(transcriptMessageID: string,
78
+        newTranscriptMessage: Object) {
79
+    return {
80
+        type: UPDATE_TRANSCRIPT_MESSAGE,
81
+        transcriptMessageID,
82
+        newTranscriptMessage
83
+    };
84
+}

+ 0
- 0
react/features/subtitles/components/TranscriptionSubtitles.native.js ファイルの表示


+ 78
- 0
react/features/subtitles/components/TranscriptionSubtitles.web.js ファイルの表示

@@ -0,0 +1,78 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+/**
7
+ * The type of the React {@code Component} props of
8
+ * {@link TranscriptionSubtitles}.
9
+ */
10
+type Props = {
11
+
12
+    /**
13
+     * Map of transcriptMessageID's with corresponding transcriptMessage.
14
+     */
15
+    _transcriptMessages: Map<string, Object>
16
+};
17
+
18
+/**
19
+ * React {@code Component} which can display speech-to-text results from
20
+ * Jigasi as subtitles.
21
+ */
22
+class TranscriptionSubtitles extends Component<Props> {
23
+
24
+    /**
25
+     * Implements React's {@link Component#render()}.
26
+     *
27
+     * @inheritdoc
28
+     * @returns {ReactElement}
29
+     */
30
+    render() {
31
+        const paragraphs = [];
32
+
33
+        for (const [ transcriptMessageID, transcriptMessage ]
34
+            of this.props._transcriptMessages) {
35
+            let text;
36
+
37
+            if (transcriptMessage) {
38
+                text = `${transcriptMessage.participantName}: `;
39
+
40
+                if (transcriptMessage.final) {
41
+                    text += transcriptMessage.final;
42
+                } else {
43
+                    const stable = transcriptMessage.stable || '';
44
+                    const unstable = transcriptMessage.unstable || '';
45
+
46
+                    text += stable + unstable;
47
+                }
48
+                paragraphs.push(
49
+                    <p key = { transcriptMessageID }> { text } </p>
50
+                );
51
+            }
52
+        }
53
+
54
+        return (
55
+            <div className = 'transcription-subtitles' >
56
+                { paragraphs }
57
+            </div>
58
+        );
59
+    }
60
+}
61
+
62
+
63
+/**
64
+ * Maps the transcriptionSubtitles in the Redux state to the associated
65
+ * props of {@code TranscriptionSubtitles}.
66
+ *
67
+ * @param {Object} state - The Redux state.
68
+ * @private
69
+ * @returns {{
70
+ *     _transcriptMessages: Map
71
+ * }}
72
+ */
73
+function _mapStateToProps(state) {
74
+    return {
75
+        _transcriptMessages: state['features/subtitles'].transcriptMessages
76
+    };
77
+}
78
+export default connect(_mapStateToProps)(TranscriptionSubtitles);

+ 1
- 0
react/features/subtitles/components/index.js ファイルの表示

@@ -0,0 +1 @@
1
+export { default as TranscriptionSubtitles } from './TranscriptionSubtitles';

+ 6
- 0
react/features/subtitles/index.js ファイルの表示

@@ -0,0 +1,6 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './components';
4
+
5
+import './middleware';
6
+import './reducer';

+ 112
- 0
react/features/subtitles/middleware.js ファイルの表示

@@ -0,0 +1,112 @@
1
+import { MiddlewareRegistry } from '../base/redux';
2
+
3
+import { ENDPOINT_MESSAGE_RECEIVED } from './actionTypes';
4
+import {
5
+    addTranscriptMessage,
6
+    removeTranscriptMessage,
7
+    updateTranscriptMessage
8
+} from './actions';
9
+
10
+const logger = require('jitsi-meet-logger').getLogger(__filename);
11
+
12
+/**
13
+* Time after which the rendered subtitles will be removed.
14
+*/
15
+const REMOVE_AFTER_MS = 3000;
16
+
17
+/**
18
+ * Middleware that catches actions related to transcript messages
19
+ * to be rendered in {@link TranscriptionSubtitles }
20
+ *
21
+ * @param {Store} store - Redux store.
22
+ * @returns {Function}
23
+ */
24
+MiddlewareRegistry.register(store => next => action => {
25
+
26
+    switch (action.type) {
27
+    case ENDPOINT_MESSAGE_RECEIVED:
28
+        return _endpointMessageReceived(store, next, action);
29
+    }
30
+
31
+    return next(action);
32
+});
33
+
34
+/**
35
+ * Notifies the feature transcription that the action
36
+ * {@code ENDPOINT_MESSAGE_RECEIVED} is being dispatched within a specific redux
37
+ * store.
38
+ *
39
+ * @param {Store} store - The redux store in which the specified {@code action}
40
+ * is being dispatched.
41
+ * @param {Dispatch} next - The redux {@code dispatch} function to
42
+ * dispatch the specified {@code action} to the specified {@code store}.
43
+ * @param {Action} action - The redux action {@code ENDPOINT_MESSAGE_RECEIVED}
44
+ * which is being dispatched in the specified {@code store}.
45
+ * @private
46
+ * @returns {Object} The value returned by {@code next(action)}.
47
+ */
48
+function _endpointMessageReceived({ dispatch, getState }, next, action) {
49
+    const json = action.json;
50
+
51
+    try {
52
+
53
+        // Let's first check if the given object has the correct
54
+        // type in the json, which identifies it as a json message sent
55
+        // from Jigasi with speech-to-to-text results
56
+        if (json.type === 'transcription-result') {
57
+            // Extract the useful data from the json.
58
+            const isInterim = json.is_interim;
59
+            const participantName = json.participant.name;
60
+            const stability = json.stability;
61
+            const text = json.transcript[0].text;
62
+            const transcriptMessageID = json.message_id;
63
+
64
+            // If this is the first result with the unique message ID,
65
+            // we add it to the state along with the name of the participant
66
+            // who said given text
67
+            if (!getState()['features/subtitles']
68
+                .transcriptMessages.has(transcriptMessageID)) {
69
+                dispatch(addTranscriptMessage(transcriptMessageID,
70
+                    participantName));
71
+            }
72
+            const { transcriptMessages } = getState()['features/subtitles'];
73
+            const newTranscriptMessage
74
+                = { ...transcriptMessages.get(transcriptMessageID) };
75
+
76
+            // If this is final result, update the state as a final result
77
+            // and start a count down to remove the subtitle from the state
78
+            if (!isInterim) {
79
+
80
+                newTranscriptMessage.final = text;
81
+                dispatch(updateTranscriptMessage(transcriptMessageID,
82
+                    newTranscriptMessage));
83
+
84
+                setTimeout(() => {
85
+                    dispatch(removeTranscriptMessage(transcriptMessageID));
86
+                }, REMOVE_AFTER_MS);
87
+            } else if (stability > 0.85) {
88
+
89
+                // If the message has a high stability, we can update the
90
+                // stable field of the state and remove the previously
91
+                // unstable results
92
+
93
+                newTranscriptMessage.stable = text;
94
+                newTranscriptMessage.unstable = undefined;
95
+                dispatch(updateTranscriptMessage(transcriptMessageID,
96
+                    newTranscriptMessage));
97
+            } else {
98
+                // Otherwise, this result has an unstable result, which we
99
+                // add to the state. The unstable result will be appended
100
+                // after the stable part.
101
+
102
+                newTranscriptMessage.unstable = text;
103
+                dispatch(updateTranscriptMessage(transcriptMessageID,
104
+                    newTranscriptMessage));
105
+            }
106
+        }
107
+    } catch (error) {
108
+        logger.error('Error occurred while updating transcriptions\n', error);
109
+    }
110
+
111
+    return next(action);
112
+}

+ 99
- 0
react/features/subtitles/reducer.js ファイルの表示

@@ -0,0 +1,99 @@
1
+import { ReducerRegistry } from '../base/redux';
2
+
3
+import {
4
+    ADD_TRANSCRIPT_MESSAGE,
5
+    REMOVE_TRANSCRIPT_MESSAGE,
6
+    UPDATE_TRANSCRIPT_MESSAGE
7
+} from './actionTypes';
8
+
9
+/**
10
+ * Default State for 'features/transcription' feature
11
+ */
12
+const defaultState = {
13
+    transcriptMessages: new Map()
14
+};
15
+
16
+/**
17
+ * Listen for actions for the transcription feature to be used by the actions
18
+ * to update the rendered transcription subtitles.
19
+ */
20
+ReducerRegistry.register('features/subtitles', (
21
+        state = defaultState, action) => {
22
+    switch (action.type) {
23
+    case ADD_TRANSCRIPT_MESSAGE:
24
+        return _addTranscriptMessage(state, action);
25
+
26
+    case REMOVE_TRANSCRIPT_MESSAGE:
27
+        return _removeTranscriptMessage(state, action);
28
+
29
+    case UPDATE_TRANSCRIPT_MESSAGE:
30
+        return _updateTranscriptMessage(state, action);
31
+    }
32
+
33
+    return state;
34
+});
35
+
36
+/**
37
+ * Reduces a specific Redux action ADD_TRANSCRIPT_MESSAGE of the feature
38
+ * transcription.
39
+ *
40
+ * @param {Object} state - The Redux state of the feature transcription.
41
+ * @param {Action} action -The Redux action ADD_TRANSCRIPT_MESSAGE to reduce.
42
+ * @returns {Object} The new state of the feature transcription after the
43
+ * reduction of the specified action.
44
+ */
45
+function _addTranscriptMessage(state,
46
+        { transcriptMessageID, participantName }) {
47
+    const newTranscriptMessages = new Map(state.transcriptMessages);
48
+
49
+    // Adds a new key,value pair to the Map once a new message arrives.
50
+    newTranscriptMessages.set(transcriptMessageID, { participantName });
51
+
52
+    return {
53
+        ...state,
54
+        transcriptMessages: newTranscriptMessages
55
+    };
56
+}
57
+
58
+/**
59
+ * Reduces a specific Redux action REMOVE_TRANSCRIPT_MESSAGE of the feature
60
+ * transcription.
61
+ *
62
+ * @param {Object} state - The Redux state of the feature transcription.
63
+ * @param {Action} action -The Redux action REMOVE_TRANSCRIPT_MESSAGE to reduce.
64
+ * @returns {Object} The new state of the feature transcription after the
65
+ * reduction of the specified action.
66
+ */
67
+function _removeTranscriptMessage(state, { transcriptMessageID }) {
68
+    const newTranscriptMessages = new Map(state.transcriptMessages);
69
+
70
+    // Deletes the key from Map once a final message arrives.
71
+    newTranscriptMessages.delete(transcriptMessageID);
72
+
73
+    return {
74
+        ...state,
75
+        transcriptMessages: newTranscriptMessages
76
+    };
77
+}
78
+
79
+/**
80
+ * Reduces a specific Redux action UPDATE_TRANSCRIPT_MESSAGE of the feature
81
+ * transcription.
82
+ *
83
+ * @param {Object} state - The Redux state of the feature transcription.
84
+ * @param {Action} action -The Redux action UPDATE_TRANSCRIPT_MESSAGE to reduce.
85
+ * @returns {Object} The new state of the feature transcription after the
86
+ * reduction of the specified action.
87
+ */
88
+function _updateTranscriptMessage(state,
89
+        { transcriptMessageID, newTranscriptMessage }) {
90
+    const newTranscriptMessages = new Map(state.transcriptMessages);
91
+
92
+    // Updates the new message for the given key in the Map.
93
+    newTranscriptMessages.set(transcriptMessageID, newTranscriptMessage);
94
+
95
+    return {
96
+        ...state,
97
+        transcriptMessages: newTranscriptMessages
98
+    };
99
+}

読み込み中…
キャンセル
保存