This commit is contained in:
22
react/features/transcribing/actionTypes.ts
Normal file
22
react/features/transcribing/actionTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* The type of Redux action triggering storage of participantId of transcriber,
|
||||
* so that it can later be kicked
|
||||
*
|
||||
* {
|
||||
* type: TRANSCRIBER_JOINED,
|
||||
* participantId: String
|
||||
* }
|
||||
* @private
|
||||
*/
|
||||
export const TRANSCRIBER_JOINED = 'TRANSCRIBER_JOINED';
|
||||
|
||||
/**
|
||||
* The type of Redux action signalling that the transcriber has left
|
||||
*
|
||||
* {
|
||||
* type: TRANSCRIBER_LEFT,
|
||||
* participantId: String
|
||||
* }
|
||||
* @private
|
||||
*/
|
||||
export const TRANSCRIBER_LEFT = 'TRANSCRIBER_LEFT';
|
||||
40
react/features/transcribing/actions.ts
Normal file
40
react/features/transcribing/actions.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
TRANSCRIBER_JOINED,
|
||||
TRANSCRIBER_LEFT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Notify that the transcriber, with a unique ID, has joined.
|
||||
*
|
||||
* @param {string} participantId - The participant id of the transcriber.
|
||||
* @returns {{
|
||||
* type: TRANSCRIBER_JOINED,
|
||||
* participantId: string
|
||||
* }}
|
||||
*/
|
||||
export function transcriberJoined(participantId: string) {
|
||||
return {
|
||||
type: TRANSCRIBER_JOINED,
|
||||
transcriberJID: participantId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that the transcriber, with a unique ID, has left.
|
||||
*
|
||||
* @param {string} participantId - The participant id of the transcriber.
|
||||
* @param {boolean} abruptly - The transcriber did not exit the conference gracefully with switching off first.
|
||||
* It maybe there was some backend problem, like network.
|
||||
* @returns {{
|
||||
* type: TRANSCRIBER_LEFT,
|
||||
* participantId: string,
|
||||
* abruptly: boolean
|
||||
* }}
|
||||
*/
|
||||
export function transcriberLeft(participantId: string, abruptly: boolean) {
|
||||
return {
|
||||
type: TRANSCRIBER_LEFT,
|
||||
transcriberJID: participantId,
|
||||
abruptly
|
||||
};
|
||||
}
|
||||
83
react/features/transcribing/functions.ts
Normal file
83
react/features/transcribing/functions.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IConfig } from '../base/config/configType';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
|
||||
import JITSI_TO_BCP47_MAP from './jitsi-bcp47-map.json';
|
||||
import logger from './logger';
|
||||
import TRANSCRIBER_LANGS from './transcriber-langs.json';
|
||||
|
||||
const DEFAULT_TRANSCRIBER_LANG = 'en-US';
|
||||
|
||||
/**
|
||||
* Determine which language to use for transcribing.
|
||||
*
|
||||
* @param {*} config - Application config.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function determineTranscriptionLanguage(config: IConfig) {
|
||||
const { transcription } = config;
|
||||
|
||||
// if transcriptions are not enabled nothing to determine
|
||||
if (!transcription?.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Depending on the config either use the language that the app automatically detected or the hardcoded
|
||||
// config BCP47 value.
|
||||
// Jitsi language detections uses custom language tags, but the transcriber expects BCP-47 compliant tags,
|
||||
// we use a mapping file to convert them.
|
||||
const bcp47Locale = transcription?.useAppLanguage ?? true
|
||||
? JITSI_TO_BCP47_MAP[i18next.language as keyof typeof JITSI_TO_BCP47_MAP]
|
||||
: transcription?.preferredLanguage;
|
||||
|
||||
// Check if the obtained language is supported by the transcriber
|
||||
let safeBCP47Locale = TRANSCRIBER_LANGS[bcp47Locale as keyof typeof TRANSCRIBER_LANGS] && bcp47Locale;
|
||||
|
||||
if (!safeBCP47Locale) {
|
||||
safeBCP47Locale = DEFAULT_TRANSCRIBER_LANG;
|
||||
logger.warn(`Transcriber language ${bcp47Locale} is not supported, using default ${DEFAULT_TRANSCRIBER_LANG}`);
|
||||
}
|
||||
|
||||
logger.info(`Transcriber language set to ${safeBCP47Locale}`);
|
||||
|
||||
return safeBCP47Locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is transcribing.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTranscribing(state: IReduxState) {
|
||||
return state['features/transcribing'].isTranscribing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a recorder transcription session running.
|
||||
* NOTE: If only the subtitles are running this function will return false.
|
||||
*
|
||||
* @param {Object} state - The redux state to search in.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRecorderTranscriptionsRunning(state: IReduxState) {
|
||||
const { metadata } = state['features/base/conference'];
|
||||
|
||||
return isTranscribing(state) && Boolean(metadata?.recording?.isTranscribingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the participant can start the transcription.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} - True if the participant can start the transcription.
|
||||
*/
|
||||
export function canAddTranscriber(state: IReduxState) {
|
||||
const { transcription } = state['features/base/config'];
|
||||
const isTranscribingAllowed = isJwtFeatureEnabled(state, MEET_FEATURES.TRANSCRIPTION, false);
|
||||
|
||||
return Boolean(transcription?.enabled) && isTranscribingAllowed;
|
||||
}
|
||||
52
react/features/transcribing/jitsi-bcp47-map.json
Normal file
52
react/features/transcribing/jitsi-bcp47-map.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"en": "en-US",
|
||||
"af": "af-ZA",
|
||||
"ar": "ar-EG",
|
||||
"bg": "bg-BG",
|
||||
"ca": "ca-ES",
|
||||
"cs": "cs-CZ",
|
||||
"da": "da-DK",
|
||||
"de": "de-DE",
|
||||
"el": "el-GR",
|
||||
"enGB": "en-GB",
|
||||
"es": "es-ES",
|
||||
"esUS": "es-US",
|
||||
"et": "et-EE",
|
||||
"eu": "eu-ES",
|
||||
"fi": "fi-FI",
|
||||
"fr": "fr-FR",
|
||||
"frCA": "fr-CA",
|
||||
"he": "iw-IL",
|
||||
"hi": "hi-IN",
|
||||
"mr": "mr-IN",
|
||||
"hr": "hr-HR",
|
||||
"hsb": "hsb-DE",
|
||||
"hu": "hu-HU",
|
||||
"hy": "hy-AM",
|
||||
"id": "id-ID",
|
||||
"it": "it-IT",
|
||||
"ja": "ja-JP",
|
||||
"ko": "ko-KR",
|
||||
"lt": "lt-LT",
|
||||
"ml": "ml-IN",
|
||||
"lv": "lv-LV",
|
||||
"nl": "nl-NL",
|
||||
"fa": "fa-IR",
|
||||
"pl": "pl-PL",
|
||||
"pt": "pt-PT",
|
||||
"ptBR": "pt-BR",
|
||||
"ru": "ru-RU",
|
||||
"ro": "ro-RO",
|
||||
"sk": "sk-SK",
|
||||
"sl": "sl-SL",
|
||||
"sr": "sr-RS",
|
||||
"sq": "sq-AL",
|
||||
"sv": "sv-SE",
|
||||
"te": "te-IN",
|
||||
"th": "th-TH",
|
||||
"tr": "tr-TR",
|
||||
"uk": "uk-UA",
|
||||
"vi": "vi-VN",
|
||||
"zhCN": "zh",
|
||||
"zhTW": "zh-TW"
|
||||
}
|
||||
3
react/features/transcribing/logger.ts
Normal file
3
react/features/transcribing/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/transcribing');
|
||||
25
react/features/transcribing/middleware.ts
Normal file
25
react/features/transcribing/middleware.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
|
||||
import { TRANSCRIBER_LEFT } from './actionTypes';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature transcribing.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ dispatch }) => next => action => {
|
||||
switch (action.type) {
|
||||
case TRANSCRIBER_LEFT:
|
||||
if (action.abruptly) {
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'transcribing.failed'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
79
react/features/transcribing/reducer.ts
Normal file
79
react/features/transcribing/reducer.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { CONFERENCE_PROPERTIES_CHANGED } from '../base/conference/actionTypes';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
TRANSCRIBER_JOINED,
|
||||
TRANSCRIBER_LEFT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Returns initial state for transcribing feature part of Redux store.
|
||||
*
|
||||
* @returns {{
|
||||
* isTranscribing: boolean,
|
||||
* transcriberJID: null
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
function _getInitialState() {
|
||||
return {
|
||||
/**
|
||||
* Indicates whether there is currently an active transcriber in the
|
||||
* room.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isTranscribing: false,
|
||||
|
||||
/**
|
||||
* The JID of the active transcriber.
|
||||
*
|
||||
* @type { string }
|
||||
*/
|
||||
transcriberJID: null
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITranscribingState {
|
||||
isTranscribing: boolean;
|
||||
transcriberJID?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/transcribing.
|
||||
*/
|
||||
ReducerRegistry.register<ITranscribingState>('features/transcribing',
|
||||
(state = _getInitialState(), action): ITranscribingState => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_PROPERTIES_CHANGED: {
|
||||
const audioRecording = action.properties?.['audio-recording-enabled'];
|
||||
|
||||
if (typeof audioRecording !== 'undefined') {
|
||||
const audioRecordingEnabled = audioRecording === 'true';
|
||||
|
||||
if (state.isTranscribing !== audioRecordingEnabled) {
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: audioRecordingEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case TRANSCRIBER_JOINED:
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: true,
|
||||
transcriberJID: action.transcriberJID
|
||||
};
|
||||
case TRANSCRIBER_LEFT:
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: false,
|
||||
transcriberJID: undefined
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
87
react/features/transcribing/subscriber.ts
Normal file
87
react/features/transcribing/subscriber.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../recording/constants';
|
||||
import { isLiveStreamingRunning, isRecordingRunning } from '../recording/functions';
|
||||
|
||||
import { isRecorderTranscriptionsRunning, isTranscribing } from './functions';
|
||||
|
||||
/**
|
||||
* Listens for transcriber status change.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isRecorderTranscriptionsRunning,
|
||||
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }) => {
|
||||
if (isRecorderTranscriptionsRunningValue) {
|
||||
maybeEmitRecordingNotification(dispatch, getState, true);
|
||||
} else {
|
||||
maybeEmitRecordingNotification(dispatch, getState, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isTranscribing,
|
||||
/* listener */ (isTranscribingValue, { getState }) => {
|
||||
if (isTranscribingValue) {
|
||||
notifyTranscribingStatusChanged(getState, true);
|
||||
} else {
|
||||
notifyTranscribingStatusChanged(getState, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Emit a recording started / stopped notification if the transcription started / stopped. Only
|
||||
* if there is no recording in progress.
|
||||
*
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {Function} getState - The Redux state.
|
||||
* @param {boolean} on - Whether the transcription is on or not.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState: IStore['getState'], on: boolean) {
|
||||
const state = getState();
|
||||
const { sessionDatas } = state['features/recording'];
|
||||
const { mode: modeConstants, status: statusConstants } = JitsiRecordingConstants;
|
||||
|
||||
if (sessionDatas.some(sd => sd.mode === modeConstants.FILE && sd.status === statusConstants.ON)) {
|
||||
// If a recording is still ongoing, don't send any notification.
|
||||
return;
|
||||
}
|
||||
|
||||
const notifyProps: INotificationProps = {
|
||||
descriptionKey: on ? 'recording.on' : 'recording.off',
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification(notifyProps, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
dispatch(playSound(on ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that transcribing has started or stopped.
|
||||
*
|
||||
* @param {Function} getState - The Redux state.
|
||||
* @param {boolean} on - True if transcribing is on, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
function notifyTranscribingStatusChanged(getState: IStore['getState'], on: boolean) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const state = getState();
|
||||
const isRecording = isRecordingRunning(state);
|
||||
const isStreaming = isLiveStreamingRunning(state);
|
||||
const mode = isRecording ? JitsiRecordingConstants.mode.FILE : JitsiRecordingConstants.mode.STREAM;
|
||||
|
||||
APP.API.notifyRecordingStatusChanged(isRecording || isStreaming, mode, undefined, on);
|
||||
APP.API.notifyTranscribingStatusChanged(on);
|
||||
}
|
||||
}
|
||||
90
react/features/transcribing/transcriber-langs.json
Normal file
90
react/features/transcribing/transcriber-langs.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"af-ZA": "Afrikaans (South Africa)",
|
||||
"id-ID": "Indonesian (Indonesia)",
|
||||
"ms-MY": "Malay (Malaysia)",
|
||||
"ca-ES": "Catalan (Spain)",
|
||||
"cs-CZ": "Czech (Czech Republic)",
|
||||
"da-DK": "Danish (Denmark)",
|
||||
"de-DE": "German (Germany)",
|
||||
"en-AU": "English (Australia)",
|
||||
"en-CA": "English (Canada)",
|
||||
"en-GB": "English (United Kingdom)",
|
||||
"en-IN": "English (India)",
|
||||
"en-IE": "English (Ireland)",
|
||||
"en-NZ": "English (New Zealand)",
|
||||
"en-PH": "English (Philippines)",
|
||||
"en-ZA": "English (South Africa)",
|
||||
"en-US": "English (United States)",
|
||||
"es-AR": "Spanish (Argentina)",
|
||||
"es-BO": "Spanish (Bolivia)",
|
||||
"es-CL": "Spanish (Chile)",
|
||||
"es-CO": "Spanish (Colombia)",
|
||||
"es-CR": "Spanish (Costa Rica)",
|
||||
"es-EC": "Spanish (Ecuador)",
|
||||
"es-SV": "Spanish (El Salvador)",
|
||||
"es-ES": "Spanish (Spain)",
|
||||
"es-US": "Spanish (United States)",
|
||||
"es-GT": "Spanish (Guatemala)",
|
||||
"es-HN": "Spanish (Honduras)",
|
||||
"es-MX": "Spanish (Mexico)",
|
||||
"es-NI": "Spanish (Nicaragua)",
|
||||
"es-PA": "Spanish (Panama)",
|
||||
"es-PY": "Spanish (Paraguay)",
|
||||
"es-PE": "Spanish (Peru)",
|
||||
"es-PR": "Spanish (Puerto Rico)",
|
||||
"es-DO": "Spanish (Dominican Republic)",
|
||||
"es-UY": "Spanish (Uruguay)",
|
||||
"es-VE": "Spanish (Venezuela)",
|
||||
"eu-ES": "Basque (Spain)",
|
||||
"fil-PH": "Filipino (Philippines)",
|
||||
"fr-CA": "French (Canada)",
|
||||
"fr-FR": "French (France)",
|
||||
"gl-ES": "Galician (Spain)",
|
||||
"hr-HR": "Croatian (Croatia)",
|
||||
"zu-ZA": "Zulu (South Africa)",
|
||||
"is-IS": "Icelandic (Iceland)",
|
||||
"it-IT": "Italian (Italy)",
|
||||
"lt-LT": "Lithuanian (Lithuania)",
|
||||
"hsb-DE": "Upper Sorbian (Germany)",
|
||||
"hu-HU": "Hungarian (Hungary)",
|
||||
"nl-NL": "Dutch (Netherlands)",
|
||||
"no-NO": "Norwegian Bokmål (Norway)",
|
||||
"pl-PL": "Polish (Poland)",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"pt-PT": "Portuguese (Portugal)",
|
||||
"ro-RO": "Romanian (Romania)",
|
||||
"sk-SK": "Slovak (Slovakia)",
|
||||
"sl-SI": "Slovenian (Slovenia)",
|
||||
"fi-FI": "Finnish (Finland)",
|
||||
"sv-SE": "Swedish (Sweden)",
|
||||
"vi-VN": "Vietnamese (Vietnam)",
|
||||
"tr-TR": "Turkish (Turkey)",
|
||||
"el-GR": "Greek (Greece)",
|
||||
"bg-BG": "Bulgarian (Bulgaria)",
|
||||
"ru-RU": "Russian (Russia)",
|
||||
"sr-RS": "Serbian (Serbia)",
|
||||
"uk-UA": "Ukrainian (Ukraine)",
|
||||
"iw-IL": "Hebrew",
|
||||
"ar-IL": "Arabic (Israel)",
|
||||
"ar-JO": "Arabic (Jordan)",
|
||||
"ar-AE": "Arabic (United Arab Emirates)",
|
||||
"ar-BH": "Arabic (Bahrain)",
|
||||
"ar-DZ": "Arabic (Algeria)",
|
||||
"ar-SA": "Arabic (Saudi Arabia)",
|
||||
"ar-IQ": "Arabic (Iraq)",
|
||||
"ar-KW": "Arabic (Kuwait)",
|
||||
"ar-MA": "Arabic (Morocco)",
|
||||
"ar-TN": "Arabic (Tunisia)",
|
||||
"ar-OM": "Arabic (Oman)",
|
||||
"ar-PS": "Arabic (State of Palestine)",
|
||||
"ar-QA": "Arabic (Qatar)",
|
||||
"ar-LB": "Arabic (Lebanon)",
|
||||
"ar-EG": "Arabic (Egypt)",
|
||||
"fa-IR": "Persian (Iran)",
|
||||
"hi-IN": "Hindi (India)",
|
||||
"th-TH": "Thai (Thailand)",
|
||||
"ko-KR": "Korean (South Korea)",
|
||||
"zh-TW": "Chinese Mandarin (Traditional, Taiwan)",
|
||||
"ja-JP": "Japanese (Japan)",
|
||||
"zh": "Chinese Mandarin (Simplified, China)"
|
||||
}
|
||||
Reference in New Issue
Block a user