init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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';

View 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
};
}

View 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;
}

View 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"
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/transcribing');

View 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);
});

View 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;
}
});

View 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);
}
}

View 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)"
}