From f4321d0e3201f3251251a4e4468b27649c1bbf64 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 9 Jun 2025 13:35:32 +0530 Subject: [PATCH 1/7] add text-track column and stt in recording modal (#720) --- audio-livecast.config.json | 3 +- audio-livecast.config.light.json | 3 +- config.json | 3 +- config.light.json | 3 +- live-streaming.config.json | 3 +- live-streaming.config.light.json | 3 +- template/defaultConfig.js | 3 +- template/global.d.ts | 1 + template/src/components/Controls.tsx | 36 +-- template/src/components/common/data-table.tsx | 22 +- .../controls/useControlPermissionMatrix.tsx | 1 + .../recordings/RecordingItemRow.tsx | 285 ++++++++++++++++++ .../recordings/RecordingsDateTable.tsx | 115 +++++-- .../recordings/TextTrackItemRow.tsx | 120 ++++++++ .../text-tracks/TextTracksTable.tsx | 12 +- .../text-tracks/useFetchSTTTranscript.tsx | 265 +++++++++------- .../subComponents/recording/useRecording.tsx | 23 +- voice-chat.config.json | 3 +- voice-chat.config.light.json | 3 +- 19 files changed, 729 insertions(+), 178 deletions(-) create mode 100644 template/src/components/recordings/RecordingItemRow.tsx create mode 100644 template/src/components/recordings/TextTrackItemRow.tsx diff --git a/audio-livecast.config.json b/audio-livecast.config.json index 2e4c58048..8e5243c0a 100644 --- a/audio-livecast.config.json +++ b/audio-livecast.config.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/audio-livecast.config.light.json b/audio-livecast.config.light.json index 4b0bfb56c..6881b77bf 100644 --- a/audio-livecast.config.light.json +++ b/audio-livecast.config.light.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/config.json b/config.json index 5cafb4c44..2d88afa99 100644 --- a/config.json +++ b/config.json @@ -98,5 +98,6 @@ "ENABLE_SPOTLIGHT": false, "AUTO_CONNECT_RTM": false, "ENABLE_CONVERSATIONAL_AI": false, - "CUSTOMIZE_AGENT": false + "CUSTOMIZE_AGENT": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/config.light.json b/config.light.json index cea0d03d5..eae16814d 100644 --- a/config.light.json +++ b/config.light.json @@ -96,5 +96,6 @@ "ENABLE_SPOTLIGHT": false, "AUTO_CONNECT_RTM": false, "ENABLE_WAITING_ROOM_AUTO_APPROVAL": true, - "ENABLE_WAITING_ROOM_AUTO_REQUEST": true + "ENABLE_WAITING_ROOM_AUTO_REQUEST": true, + "ENABLE_TEXT_TRACKS": true } diff --git a/live-streaming.config.json b/live-streaming.config.json index 4af2cf44d..92b69ea33 100644 --- a/live-streaming.config.json +++ b/live-streaming.config.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/live-streaming.config.light.json b/live-streaming.config.light.json index 1f90ba71e..e93a3e2f0 100644 --- a/live-streaming.config.light.json +++ b/live-streaming.config.light.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/template/defaultConfig.js b/template/defaultConfig.js index c0fea119c..b22bcf11c 100644 --- a/template/defaultConfig.js +++ b/template/defaultConfig.js @@ -89,7 +89,8 @@ const DefaultConfig = { AI_LAYOUT: 'LAYOUT_TYPE_1', SDK_CODEC: 'vp8', ENABLE_WAITING_ROOM_AUTO_APPROVAL: false, - ENABLE_WAITING_ROOM_AUTO_REQUEST: false + ENABLE_WAITING_ROOM_AUTO_REQUEST: false, + ENABLE_TEXT_TRACKS: true, }; module.exports = DefaultConfig; diff --git a/template/global.d.ts b/template/global.d.ts index d20a174ab..cf136cf05 100644 --- a/template/global.d.ts +++ b/template/global.d.ts @@ -177,6 +177,7 @@ interface ConfigInterface { SDK_CODEC: string; ENABLE_WAITING_ROOM_AUTO_APPROVAL: boolean; ENABLE_WAITING_ROOM_AUTO_REQUEST: boolean; + ENABLE_TEXT_TRACKS: boolean; } declare var $config: ConfigInterface; declare module 'customization' { diff --git a/template/src/components/Controls.tsx b/template/src/components/Controls.tsx index fbedf9b18..0828af9e2 100644 --- a/template/src/components/Controls.tsx +++ b/template/src/components/Controls.tsx @@ -817,22 +817,22 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 13. Text-tracks to download - // const canAccessAllTextTracks = - // useControlPermissionMatrix('viewAllTextTracks'); - - // if (canAccessAllTextTracks) { - // actionMenuitems.push({ - // componentName: 'view-all-text-tracks', - // order: 13, - // icon: 'transcript', - // iconColor: $config.SECONDARY_ACTION_COLOR, - // textColor: $config.FONT_COLOR, - // title: viewTextTracksLabel, - // onPress: () => { - // toggleTextTrackModal(); - // }, - // }); - // } + const canAccessAllTextTracks = + useControlPermissionMatrix('viewAllTextTracks'); + + if (canAccessAllTextTracks) { + actionMenuitems.push({ + componentName: 'view-all-text-tracks', + order: 13, + icon: 'transcript', + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.FONT_COLOR, + title: viewTextTracksLabel, + onPress: () => { + toggleTextTrackModal(); + }, + }); + } useEffect(() => { if (isHovered) { @@ -980,11 +980,11 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { )} )} - {/* {canAccessAllTextTracks && isTextTrackModalOpen ? ( + {canAccessAllTextTracks && isTextTrackModalOpen ? ( ) : ( <> - )} */} + )} = ({ export {TableHeader, TableFooter, TableBody}; -const style = StyleSheet.create({ +export const style = StyleSheet.create({ scrollgrow: { flexGrow: 1, }, @@ -249,7 +249,6 @@ const style = StyleSheet.create({ flex: 1, alignSelf: 'center', justifyContent: 'center', - // height: 100, gap: 10, }, tpreview: { @@ -275,6 +274,8 @@ const style = StyleSheet.create({ tactions: { display: 'flex', flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', }, tlink: { color: $config.PRIMARY_ACTION_BRAND_COLOR, @@ -382,4 +383,21 @@ const style = StyleSheet.create({ pl15: { paddingLeft: 15, }, + // icon celles + tdIconCell: { + flex: 0, + flexShrink: 0, + alignItems: 'flex-start', + justifyContent: 'center', + minWidth: 50, + paddingRight: 50 + 12, + }, + thIconCell: { + flex: 0, + flexShrink: 0, + alignSelf: 'stretch', + justifyContent: 'center', + minWidth: 50, + paddingHorizontal: 12, + }, }); diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 568fb5fde..2daa52cf0 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -40,6 +40,7 @@ export const controlPermissionMatrix: Record< isHost && $config.ENABLE_STT && $config.ENABLE_MEETING_TRANSCRIPT && + $config.ENABLE_TEXT_TRACKS && isWeb(), }; diff --git a/template/src/components/recordings/RecordingItemRow.tsx b/template/src/components/recordings/RecordingItemRow.tsx new file mode 100644 index 000000000..fcb14fd67 --- /dev/null +++ b/template/src/components/recordings/RecordingItemRow.tsx @@ -0,0 +1,285 @@ +import React, {useEffect, useState} from 'react'; +import {View, Text, Linking, TouchableOpacity, StyleSheet} from 'react-native'; +import {downloadRecording, getDuration, getRecordedDateTime} from './utils'; +import IconButtonWithToolTip from '../../atoms/IconButton'; +import Tooltip from '../../atoms/Tooltip'; +import Clipboard from '../../subComponents/Clipboard'; +import Spacer from '../../atoms/Spacer'; +import PlatformWrapper from '../../utils/PlatformWrapper'; +import {useFetchSTTTranscript} from '../text-tracks/useFetchSTTTranscript'; +import {style} from '../common/data-table'; +import {FetchRecordingData} from '../../subComponents/recording/useRecording'; +import ImageIcon from '../../atoms/ImageIcon'; +import TextTrackItemRow from './TextTrackItemRow'; + +interface RecordingItemRowProps { + item: FetchRecordingData['recordings'][0]; + onDeleteAction: (id: string) => void; + onTextTrackDownload: (textTrackLink: string) => void; +} +export default function RecordingItemRow({ + item, + onDeleteAction, + onTextTrackDownload, +}: RecordingItemRowProps) { + const [expanded, setIsExpanded] = useState(false); + + const [date, time] = getRecordedDateTime(item.created_at); + const recordingStatus = item.status; + + const {sttRecState, getSTTsForRecording} = useFetchSTTTranscript(); + const { + status, + error, + data: {stts = []}, + } = sttRecState; + console.log('supriya sttRecState', sttRecState); + useEffect(() => { + if (expanded) { + if (item.id) { + getSTTsForRecording(item.id); + } + } + }, [expanded, item.id, getSTTsForRecording]); + + if ( + recordingStatus === 'STOPPING' || + recordingStatus === 'STARTED' || + (recordingStatus === 'INPROGRESS' && !item?.download_url) + ) { + return ( + + + + + Current recording is ongoing. Once it concludes, we'll generate the + link + + + + ); + } + + // Collapsible Row + return ( + + {/* ========== PARENT ROW ========== */} + + + setIsExpanded(prev => !prev)} + /> + + + + {date} +
+ {time} +
+
+ + + {getDuration(item.created_at, item.ended_at)} + + + + {!item.download_url ? ( + + {'No recording found'} + + ) : item?.download_url?.length > 0 ? ( + + + {item?.download_url?.map((link: string, i: number) => ( + = 1 ? {marginTop: 8} : {}, + ]}> + + { + downloadRecording(link); + }} + /> + + + { + if (await Linking.canOpenURL(link)) { + await Linking.openURL(link); + } + }} + /> + + + { + Clipboard.setString(link); + }} + toolTipIcon={ + <> + + + + } + fontSize={12} + renderContent={() => { + return ( + + {(isHovered: boolean) => ( + { + Clipboard.setString(link); + }}> + + + )} + + ); + }} + /> + + + ))} + + + { + onDeleteAction && onDeleteAction(item.id); + }} + /> + + + ) : ( + + No recordings found + + )} + +
+ {/* ========== CHILDREN ROW ========== */} + {expanded && ( + + + STT's + + + {status === 'idle' || status === 'pending' ? ( + Fetching text-tracks.... + ) : status === 'rejected' ? ( + + {error?.message || + 'There was an error while fetching the text-tracks'} + + ) : status === 'resolved' && stts?.length === 0 ? ( + + There are no STT's for this recording + + ) : ( + <> + Found {stts.length} text tracks + + {stts.map(item => ( + + ))} + + + )} + + + )} +
+ ); +} + +const expanedStyles = StyleSheet.create({ + expandedContainer: { + display: 'flex', + flexDirection: 'column', + gap: 5, + color: $config.FONT_COLOR, + borderColor: $config.CARD_LAYER_3_COLOR, + backgroundColor: $config.CARD_LAYER_2_COLOR, + paddingHorizontal: 12, + paddingVertical: 15, + borderRadius: 5, + }, + expandedHeaderText: { + fontSize: 15, + lineHeight: 32, + fontWeight: '500', + color: $config.FONT_COLOR, + }, + expandedHeaderBody: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, +}); diff --git a/template/src/components/recordings/RecordingsDateTable.tsx b/template/src/components/recordings/RecordingsDateTable.tsx index 6089cdb96..5084bc55f 100644 --- a/template/src/components/recordings/RecordingsDateTable.tsx +++ b/template/src/components/recordings/RecordingsDateTable.tsx @@ -1,30 +1,65 @@ import React, {useState, useEffect} from 'react'; import {View, Text} from 'react-native'; -import {style} from './style'; -import {RTableHeader, RTableBody, RTableFooter} from './recording-table'; -import {useRecording} from '../../subComponents/recording/useRecording'; +import { + APIStatus, + FetchRecordingData, + useRecording, +} from '../../subComponents/recording/useRecording'; import events from '../../rtm-events-api'; import {EventNames} from '../../rtm-events'; +import {style, TableBody, TableHeader} from '../common/data-table'; +import Loading from '../../subComponents/Loading'; +import ImageIcon from '../../atoms/ImageIcon'; +import RecordingItemRow from './RecordingItemRow'; +import GenericPopup from '../common/GenericPopup'; +import {downloadS3Link} from '../../utils/common'; + +function EmptyRecordingState() { + return ( + + + + + + + No recording found for this meeting + + + + ); +} + +const headers = ['', 'Date/Time', 'Duration', 'Actions']; +const defaultPageNumber = 1; function RecordingsDateTable(props) { - const [state, setState] = React.useState({ + const [state, setState] = React.useState<{ + status: APIStatus; + data: { + recordings: FetchRecordingData['recordings']; + pagination: FetchRecordingData['pagination']; + }; + error: Error; + }>({ status: 'idle', data: { - pagination: {}, recordings: [], + pagination: {total: 0, limit: 10, page: defaultPageNumber}, }, error: null, }); - const { - status, - data: {pagination, recordings}, - error, - } = state; + + const [currentPage, setCurrentPage] = useState(defaultPageNumber); const {fetchRecordings} = useRecording(); - const defaultPageNumber = 1; - const [currentPage, setCurrentPage] = useState(defaultPageNumber); + // message for any download‐error popup + const [errorSnack, setErrorSnack] = React.useState(); const onRecordingDeleteCallback = () => { setCurrentPage(defaultPageNumber); @@ -38,7 +73,7 @@ function RecordingsDateTable(props) { }; }, []); - const getRecordings = pageNumber => { + const getRecordings = (pageNumber: number) => { setState(prev => ({...prev, status: 'pending'})); fetchRecordings(pageNumber).then( response => @@ -47,8 +82,13 @@ function RecordingsDateTable(props) { status: 'resolved', data: { recordings: response?.recordings || [], - pagination: response?.pagination || {}, + pagination: response?.pagination || { + total: 0, + limit: 10, + page: defaultPageNumber, + }, }, + error: null, })), error => setState(prev => ({...prev, status: 'rejected', error})), ); @@ -58,26 +98,49 @@ function RecordingsDateTable(props) { getRecordings(currentPage); }, [currentPage]); - if (status === 'rejected') { + if (state.status === 'rejected') { return ( - {error?.message} + {state.error?.message} ); } + const onTextTrackDownload = (textTrackLink: string) => { + downloadS3Link(textTrackLink).catch((err: Error) => { + setErrorSnack(err.message || 'Download failed'); + }); + }; + return ( - - - + + } + renderRow={item => ( + + )} + emptyComponent={} /> + {/** ERROR POPUP **/} + {errorSnack && ( + setErrorSnack(undefined)} + onConfirm={() => setErrorSnack(undefined)} + /> + )} ); } diff --git a/template/src/components/recordings/TextTrackItemRow.tsx b/template/src/components/recordings/TextTrackItemRow.tsx new file mode 100644 index 000000000..a028dcc09 --- /dev/null +++ b/template/src/components/recordings/TextTrackItemRow.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import {View, Text, TouchableOpacity} from 'react-native'; +import IconButtonWithToolTip from '../../atoms/IconButton'; +import Tooltip from '../../atoms/Tooltip'; +import Clipboard from '../../subComponents/Clipboard'; +import Spacer from '../../atoms/Spacer'; +import PlatformWrapper from '../../utils/PlatformWrapper'; +import {FetchSTTTranscriptResponse} from '../text-tracks/useFetchSTTTranscript'; +import {style} from '../common/data-table'; +import ImageIcon from '../../atoms/ImageIcon'; + +interface TextTrackItemRowProps { + item: FetchSTTTranscriptResponse['stts'][0]; + onTextTrackDownload: (link: string) => void; +} + +export default function TextTrackItemRow({ + item, + onTextTrackDownload, +}: TextTrackItemRowProps) { + const textTrackStatus = item.status; + + return ( + + {!item.download_url ? ( + + {textTrackStatus === 'STOPPING' || + textTrackStatus === 'STARTED' || + (textTrackStatus === 'INPROGRESS' && !item?.download_url) ? ( + + {'The link will be generated once the meeting ends'} + + ) : ( + {'No text-tracks found'} + )} + + ) : item?.download_url?.length > 0 ? ( + + + {item?.download_url?.map((link: string, i: number) => ( + = 1 ? {marginTop: 8} : {}, + ]}> + + { + onTextTrackDownload && onTextTrackDownload(link); + }} + /> + + + { + Clipboard.setString(link); + }} + toolTipIcon={ + <> + + + + } + fontSize={12} + renderContent={() => { + return ( + + {(isHovered: boolean) => ( + { + Clipboard.setString(link); + }}> + + + )} + + ); + }} + /> + + + ))} + + + ) : ( + + No text-tracks found + + )} + + ); +} diff --git a/template/src/components/text-tracks/TextTracksTable.tsx b/template/src/components/text-tracks/TextTracksTable.tsx index afcc7f33a..ff28e212e 100644 --- a/template/src/components/text-tracks/TextTracksTable.tsx +++ b/template/src/components/text-tracks/TextTracksTable.tsx @@ -204,16 +204,14 @@ function ErrorTextTrackState({message}: {message: string}) { } function TextTracksTable() { + const {sttState, currentPage, setCurrentPage, deleteTranscript} = + useFetchSTTTranscript(); + const { status, - stts, - pagination, + data: {stts, pagination}, error: fetchTranscriptError, - currentPage, - setCurrentPage, - deleteTranscript, - } = useFetchSTTTranscript(); - + } = sttState; // id of text-tracj to delete const [textTrackIdToDelete, setTextTrackIdToDelete] = React.useState< string | undefined diff --git a/template/src/components/text-tracks/useFetchSTTTranscript.tsx b/template/src/components/text-tracks/useFetchSTTTranscript.tsx index 838f7e87f..7160f62ac 100644 --- a/template/src/components/text-tracks/useFetchSTTTranscript.tsx +++ b/template/src/components/text-tracks/useFetchSTTTranscript.tsx @@ -5,11 +5,7 @@ import getUniqueID from '../../utils/getUniqueID'; import {logger, LogSource} from '../../logger/AppBuilderLogger'; export interface FetchSTTTranscriptResponse { - pagination: { - limit: number; - total: number; - page: number; - }; + pagination: {limit: number; total: number; page: number}; stts: { id: string; download_url: string[]; @@ -23,118 +19,114 @@ export interface FetchSTTTranscriptResponse { export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected'; -export function useFetchSTTTranscript(defaultLimit = 10) { +export function useFetchSTTTranscript() { const { data: {roomId}, } = useRoomInfo(); const {store} = useContext(StorageContext); + const [currentPage, setCurrentPage] = useState(1); - const [state, setState] = useState<{ + const [sttState, setSttState] = useState<{ status: APIStatus; data: { stts: FetchSTTTranscriptResponse['stts']; pagination: FetchSTTTranscriptResponse['pagination']; }; - error: Error; + error: Error | null; }>({ status: 'idle', - data: {stts: [], pagination: {total: 0, limit: defaultLimit, page: 1}}, + data: {stts: [], pagination: {total: 0, limit: 10, page: 1}}, error: null, }); - const fetchStts = useCallback( - async (page: number) => { - const requestId = getUniqueID(); - const start = Date.now(); + //–– by‐recording state –– + const [sttRecState, setSttRecState] = useState<{ + status: APIStatus; + data: {stts: FetchSTTTranscriptResponse['stts']}; + error: Error | null; + }>({ + status: 'idle', + data: { + stts: [], + }, + error: null, + }); - try { - if (!roomId?.host) { - const error = new Error('room id is empty'); - return Promise.reject(error); - } - const res = await fetch( - `${$config.BACKEND_ENDPOINT}/v1/stt-transcript`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, - 'X-Session-Id': logger.getSessionId(), - }, - body: JSON.stringify({ - passphrase: roomId.host, - limit: defaultLimit, - page, - }), - }, - ); - const json = await res.json(); - const end = Date.now(); + const getSTTs = useCallback( + (page: number) => { + setSttState(s => ({...s, status: 'pending', error: null})); + const reqId = getUniqueID(); + const start = Date.now(); - if (!res.ok) { - logger.error( + fetch(`${$config.BACKEND_ENDPOINT}/v1/stt-transcript`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': reqId, + 'X-Session-Id': logger.getSessionId(), + }, + body: JSON.stringify({ + passphrase: roomId.host, + limit: 10, + page, + }), + }) + .then(async res => { + const json = await res.json(); + const end = Date.now(); + if (!res.ok) { + logger.error( + LogSource.NetworkRest, + 'stt-transcript', + 'Fetch STT transcripts failed', + { + json, + start, + end, + latency: end - start, + requestId: reqId, + }, + ); + throw new Error(json?.error?.message || res.statusText); + } + logger.debug( LogSource.NetworkRest, 'stt-transcript', - 'Fetching STT transcripts failed', + 'Fetch STT transcripts succeeded', { json, start, end, latency: end - start, - requestId, + requestId: reqId, }, ); - throw new Error(json?.error?.message || 'Unknown fetch error'); - } - - logger.debug( - LogSource.NetworkRest, - 'stt-transcript', - 'Fetched STT transcripts', - { - json, - start, - end, - latency: end - start, - requestId, - }, - ); - return json; - } catch (err) { - return Promise.reject(err); - } - }, - [roomId.host, store.token, defaultLimit], - ); - - const getSTTs = useCallback( - (page: number) => { - setState(s => ({...s, status: 'pending'})); - fetchStts(page).then( - data => - setState({ + return json as FetchSTTTranscriptResponse; + }) + .then(({stts = [], pagination = {total: 0, limit: 10, page}}) => { + setSttState({ status: 'resolved', - data: { - stts: data.stts || [], - pagination: data.pagination || { - total: 0, - limit: defaultLimit, - page: 1, - }, - }, + data: {stts, pagination}, error: null, - }), - err => setState(s => ({...s, status: 'rejected', error: err})), - ); + }); + }) + .catch(err => { + setSttState(s => ({...s, status: 'rejected', error: err})); + }); }, - [fetchStts, defaultLimit], + [roomId.host, store.token], ); + useEffect(() => { + getSTTs(currentPage); + }, [currentPage, getSTTs]); + + // Delete stts const deleteTranscript = useCallback( async (id: string) => { - const requestId = getUniqueID(); + const reqId = getUniqueID(); const start = Date.now(); const res = await fetch( @@ -148,7 +140,7 @@ export function useFetchSTTTranscript(defaultLimit = 10) { headers: { 'Content-Type': 'application/json', authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, + 'X-Request-Id': reqId, 'X-Session-Id': logger.getSessionId(), }, }, @@ -159,44 +151,33 @@ export function useFetchSTTTranscript(defaultLimit = 10) { logger.error( LogSource.NetworkRest, 'stt-transcript', - 'Deleting STT transcripts failed', - { - json: '', - start, - end, - latency: end - start, - requestId, - }, + 'Delete transcript failed', + {start, end, latency: end - start, requestId: reqId}, ); throw new Error(`Delete failed (${res.status})`); } logger.debug( LogSource.NetworkRest, 'stt-transcript', - 'Deleted STT transcripts', - { - json: '', - start, - end, - latency: end - start, - requestId, - }, + 'Delete transcript succeeded', + {start, end, latency: end - start, requestId: reqId}, ); - // optimistic update local state: - setState(prev => { + + // optimistic remove from paginated list + setSttState(prev => { // remove the deleted item const newStts = prev.data.stts.filter(item => item.id !== id); // decrement total count const newTotal = Math.max(prev.data.pagination.total - 1, 0); - // if we just removed the *last* item on this page, go back a page let newPage = prev.data.pagination.page; if (prev.data.stts.length === 1 && newPage > 1) { - newPage = newPage - 1; + newPage--; } return { ...prev, data: { stts: newStts, + pagination: { ...prev.data.pagination, total: newTotal, @@ -206,20 +187,80 @@ export function useFetchSTTTranscript(defaultLimit = 10) { }; }); }, - [roomId.host, store?.token], + [roomId.host, store.token], ); - useEffect(() => { - getSTTs(currentPage); - }, [currentPage, getSTTs]); + //–– fetch for a given recording –– + const getSTTsForRecording = useCallback( + (recordingId: string) => { + setSttRecState(r => ({...r, status: 'pending', error: null})); + const reqId = getUniqueID(); + const start = Date.now(); + + fetch(`${$config.BACKEND_ENDPOINT}/v1/recording/stt-transcript`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': reqId, + 'X-Session-Id': logger.getSessionId(), + }, + body: JSON.stringify({ + project_id: $config.PROJECT_ID, + recording_id: recordingId, + }), + }) + .then(async res => { + const json = await res.json(); + const end = Date.now(); + console.log('supriua json', json); + if (!res.ok) { + logger.error( + LogSource.NetworkRest, + 'stt-transcript', + 'Fetch stt-by-recording failed', + {json, start, end, latency: end - start, requestId: reqId}, + ); + throw new Error(json?.error?.message || res.statusText); + } + logger.debug( + LogSource.NetworkRest, + 'stt-transcript', + 'Fetch stt-by-recording succeeded', + {json, start, end, latency: end - start, requestId: reqId}, + ); + if (json?.error) { + logger.debug( + LogSource.NetworkRest, + 'stt-transcript', + `No STT records found (code ${json.error.code}): ${json.error.message}`, + {start, end, latency: end - start, reqId}, + ); + return []; + } else { + return json as FetchSTTTranscriptResponse['stts']; + } + }) + .then(stts => + setSttRecState({status: 'resolved', data: {stts}, error: null}), + ) + .catch(err => + setSttRecState(r => ({...r, status: 'rejected', error: err})), + ); + }, + [store.token], + ); return { - status: state.status as APIStatus, - stts: state.data.stts, - pagination: state.data.pagination, - error: state.error, + // stt list + sttState, + getSTTs, currentPage, setCurrentPage, + // STT per recording + sttRecState, + getSTTsForRecording, + // delete deleteTranscript, }; } diff --git a/template/src/subComponents/recording/useRecording.tsx b/template/src/subComponents/recording/useRecording.tsx index 1bf0445cc..e8513bfe9 100644 --- a/template/src/subComponents/recording/useRecording.tsx +++ b/template/src/subComponents/recording/useRecording.tsx @@ -67,16 +67,31 @@ const getFrontendUrl = (url: string) => { return url; }; -interface RecordingsData { - recordings: []; - pagination: {}; +export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected'; + +export interface FetchRecordingData { + pagination: { + limit: number; + total: number; + page: number; + }; + recordings: { + id: string; + download_url: string[]; + title: string; + product_name: string; + status: 'COMPLETED' | 'STARTED' | 'INPROGRESS' | 'STOPPING'; + created_at: string; + ended_at: string; + }[]; } + export interface RecordingContextInterface { startRecording: () => void; stopRecording: () => void; isRecordingActive: boolean; inProgress: boolean; - fetchRecordings?: (page: number) => Promise; + fetchRecordings?: (page: number) => Promise; deleteRecording?: (id: number) => Promise; } diff --git a/voice-chat.config.json b/voice-chat.config.json index 8afd8eb68..65bd439bf 100644 --- a/voice-chat.config.json +++ b/voice-chat.config.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } diff --git a/voice-chat.config.light.json b/voice-chat.config.light.json index 06730f2e3..98cf8496c 100644 --- a/voice-chat.config.light.json +++ b/voice-chat.config.light.json @@ -78,5 +78,6 @@ "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false + "AUTO_CONNECT_RTM": false, + "ENABLE_TEXT_TRACKS": true } From cfa6d2c0974b73dd6a4567a7ba2e160c30b3f4de Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 10 Jun 2025 16:29:12 +0530 Subject: [PATCH 2/7] add styling changes (#721) --- template/src/components/common/data-table.tsx | 39 ++++++++++++------- .../recordings/RecordingItemRow.tsx | 6 +-- .../recordings/RecordingsDateTable.tsx | 6 ++- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/template/src/components/common/data-table.tsx b/template/src/components/common/data-table.tsx index 5ba87afa7..33a1bdd65 100644 --- a/template/src/components/common/data-table.tsx +++ b/template/src/components/common/data-table.tsx @@ -19,6 +19,7 @@ interface TableHeaderProps { rowStyle?: ViewStyle; cellStyle?: ViewStyle; firstCellStyle?: ViewStyle; + lastCellStyle?: ViewStyle; textStyle?: TextStyle; } @@ -28,22 +29,27 @@ const TableHeader: React.FC = ({ rowStyle, cellStyle, firstCellStyle, + lastCellStyle, textStyle, }) => ( - {columns.map((col, index) => ( - - {col} - - ))} + {columns.map((col, index) => { + const isFirst = index === 0; + const isLast = index === (columns.length > 1 ? columns.length - 1 : 0); + return ( + + {col} + + ); + })} ); @@ -222,7 +228,7 @@ export const style = StyleSheet.create({ flex: 1, alignSelf: 'stretch', justifyContent: 'center', - paddingHorizontal: 12, + // paddingHorizontal: 12, }, thText: { color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, @@ -389,8 +395,8 @@ export const style = StyleSheet.create({ flexShrink: 0, alignItems: 'flex-start', justifyContent: 'center', - minWidth: 50, - paddingRight: 50 + 12, + minWidth: 52, + // paddingRight: 50 + 12, }, thIconCell: { flex: 0, @@ -400,4 +406,7 @@ export const style = StyleSheet.create({ minWidth: 50, paddingHorizontal: 12, }, + alignCellToRight: { + alignItems: 'flex-end', + }, }); diff --git a/template/src/components/recordings/RecordingItemRow.tsx b/template/src/components/recordings/RecordingItemRow.tsx index fcb14fd67..3e9e67cb2 100644 --- a/template/src/components/recordings/RecordingItemRow.tsx +++ b/template/src/components/recordings/RecordingItemRow.tsx @@ -33,7 +33,7 @@ export default function RecordingItemRow({ error, data: {stts = []}, } = sttRecState; - console.log('supriya sttRecState', sttRecState); + useEffect(() => { if (expanded) { if (item.id) { @@ -223,7 +223,7 @@ export default function RecordingItemRow({ {expanded && ( - STT's + Text-tracks {status === 'idle' || status === 'pending' ? ( @@ -235,7 +235,7 @@ export default function RecordingItemRow({ ) : status === 'resolved' && stts?.length === 0 ? ( - There are no STT's for this recording + There are no text-tracks's for this recording ) : ( <> diff --git a/template/src/components/recordings/RecordingsDateTable.tsx b/template/src/components/recordings/RecordingsDateTable.tsx index 5084bc55f..eb48b9185 100644 --- a/template/src/components/recordings/RecordingsDateTable.tsx +++ b/template/src/components/recordings/RecordingsDateTable.tsx @@ -113,7 +113,11 @@ function RecordingsDateTable(props) { return ( - + Date: Wed, 11 Jun 2025 11:55:16 +0530 Subject: [PATCH 3/7] add stt disabled logic in recording modal (#722) --- .../recordings/RecordingItemRow.tsx | 32 +++++++++++-------- .../recordings/RecordingsDateTable.tsx | 11 +++++-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/template/src/components/recordings/RecordingItemRow.tsx b/template/src/components/recordings/RecordingItemRow.tsx index 3e9e67cb2..ba4841321 100644 --- a/template/src/components/recordings/RecordingItemRow.tsx +++ b/template/src/components/recordings/RecordingItemRow.tsx @@ -16,11 +16,13 @@ interface RecordingItemRowProps { item: FetchRecordingData['recordings'][0]; onDeleteAction: (id: string) => void; onTextTrackDownload: (textTrackLink: string) => void; + showTextTracks: boolean; } export default function RecordingItemRow({ item, onDeleteAction, onTextTrackDownload, + showTextTracks = false, }: RecordingItemRowProps) { const [expanded, setIsExpanded] = useState(false); @@ -70,20 +72,22 @@ export default function RecordingItemRow({ {/* ========== PARENT ROW ========== */} - - setIsExpanded(prev => !prev)} - /> - + {showTextTracks && ( + + setIsExpanded(prev => !prev)} + /> + + )} {date} diff --git a/template/src/components/recordings/RecordingsDateTable.tsx b/template/src/components/recordings/RecordingsDateTable.tsx index eb48b9185..700f050bc 100644 --- a/template/src/components/recordings/RecordingsDateTable.tsx +++ b/template/src/components/recordings/RecordingsDateTable.tsx @@ -13,6 +13,7 @@ import ImageIcon from '../../atoms/ImageIcon'; import RecordingItemRow from './RecordingItemRow'; import GenericPopup from '../common/GenericPopup'; import {downloadS3Link} from '../../utils/common'; +import {useControlPermissionMatrix} from '../controls/useControlPermissionMatrix'; function EmptyRecordingState() { return ( @@ -34,7 +35,6 @@ function EmptyRecordingState() { ); } -const headers = ['', 'Date/Time', 'Duration', 'Actions']; const defaultPageNumber = 1; function RecordingsDateTable(props) { @@ -57,6 +57,8 @@ function RecordingsDateTable(props) { const [currentPage, setCurrentPage] = useState(defaultPageNumber); const {fetchRecordings} = useRecording(); + const canAccessAllTextTracks = + useControlPermissionMatrix('viewAllTextTracks'); // message for any download‐error popup const [errorSnack, setErrorSnack] = React.useState(); @@ -111,11 +113,15 @@ function RecordingsDateTable(props) { }); }; + const headers = canAccessAllTextTracks + ? ['', 'Date/Time', 'Duration', 'Actions'] + : ['Date/Time', 'Duration', 'Actions']; + return ( )} emptyComponent={} From 0e1bffd843ccbbc3f36f3178259bb1fbbb2ee2ba Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Wed, 11 Jun 2025 13:34:29 +0530 Subject: [PATCH 4/7] move the useeffect to avoid redundant api calls (#723) --- template/src/components/text-tracks/TextTracksTable.tsx | 9 +++++++-- .../src/components/text-tracks/useFetchSTTTranscript.tsx | 4 ---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/template/src/components/text-tracks/TextTracksTable.tsx b/template/src/components/text-tracks/TextTracksTable.tsx index ff28e212e..fca0afdb7 100644 --- a/template/src/components/text-tracks/TextTracksTable.tsx +++ b/template/src/components/text-tracks/TextTracksTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import {View, Text, TouchableOpacity} from 'react-native'; import Tooltip from '../../atoms/Tooltip'; import Clipboard from '../../subComponents/Clipboard'; @@ -204,7 +204,7 @@ function ErrorTextTrackState({message}: {message: string}) { } function TextTracksTable() { - const {sttState, currentPage, setCurrentPage, deleteTranscript} = + const {getSTTs, sttState, currentPage, setCurrentPage, deleteTranscript} = useFetchSTTTranscript(); const { @@ -212,6 +212,11 @@ function TextTracksTable() { data: {stts, pagination}, error: fetchTranscriptError, } = sttState; + + useEffect(() => { + getSTTs(currentPage); + }, [currentPage, getSTTs]); + // id of text-tracj to delete const [textTrackIdToDelete, setTextTrackIdToDelete] = React.useState< string | undefined diff --git a/template/src/components/text-tracks/useFetchSTTTranscript.tsx b/template/src/components/text-tracks/useFetchSTTTranscript.tsx index 7160f62ac..edaf673b7 100644 --- a/template/src/components/text-tracks/useFetchSTTTranscript.tsx +++ b/template/src/components/text-tracks/useFetchSTTTranscript.tsx @@ -119,10 +119,6 @@ export function useFetchSTTTranscript() { [roomId.host, store.token], ); - useEffect(() => { - getSTTs(currentPage); - }, [currentPage, getSTTs]); - // Delete stts const deleteTranscript = useCallback( async (id: string) => { From e37c7f17e53cac0d1b1803cd86f3011053b7cdec Mon Sep 17 00:00:00 2001 From: HariharanIT Date: Thu, 12 Jun 2025 10:28:13 +0530 Subject: [PATCH 5/7] Added rime integration --- config.json | 3 +- template/bridge/rtc/webNg/RtcEngine.ts | 7 +- template/defaultConfig.js | 1 + template/global.d.ts | 1 + template/src/pages/test.ts | 4 + .../caption/useStreamMessageUtils.ts | 17 ++- template/src/utils/testData.ts | 4 + template/src/utils/useTextToVoice.ts | 112 ++++++++++++++++++ 8 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 template/src/pages/test.ts create mode 100644 template/src/utils/testData.ts create mode 100644 template/src/utils/useTextToVoice.ts diff --git a/config.json b/config.json index 2d88afa99..678699308 100644 --- a/config.json +++ b/config.json @@ -99,5 +99,6 @@ "AUTO_CONNECT_RTM": false, "ENABLE_CONVERSATIONAL_AI": false, "CUSTOMIZE_AGENT": false, - "ENABLE_TEXT_TRACKS": true + "ENABLE_TEXT_TRACKS": true, + "PLAY_REMOTE_AUDIO": false } diff --git a/template/bridge/rtc/webNg/RtcEngine.ts b/template/bridge/rtc/webNg/RtcEngine.ts index 761910582..78ea9cfbf 100644 --- a/template/bridge/rtc/webNg/RtcEngine.ts +++ b/template/bridge/rtc/webNg/RtcEngine.ts @@ -752,8 +752,11 @@ export default class RtcEngine { // If the subscribed track is an audio track if (mediaType === 'audio') { const audioTrack = user.audioTrack; - // Play the audio - audioTrack?.play(); + if ($config.PLAY_REMOTE_AUDIO) { + // Play the audio + audioTrack?.play(); + } + this.remoteStreams.set(user.uid, { ...this.remoteStreams.get(user.uid), audio: audioTrack, diff --git a/template/defaultConfig.js b/template/defaultConfig.js index b22bcf11c..fbd5add68 100644 --- a/template/defaultConfig.js +++ b/template/defaultConfig.js @@ -91,6 +91,7 @@ const DefaultConfig = { ENABLE_WAITING_ROOM_AUTO_APPROVAL: false, ENABLE_WAITING_ROOM_AUTO_REQUEST: false, ENABLE_TEXT_TRACKS: true, + PLAY_REMOTE_AUDIO: true, }; module.exports = DefaultConfig; diff --git a/template/global.d.ts b/template/global.d.ts index cf136cf05..aaa966ff9 100644 --- a/template/global.d.ts +++ b/template/global.d.ts @@ -178,6 +178,7 @@ interface ConfigInterface { ENABLE_WAITING_ROOM_AUTO_APPROVAL: boolean; ENABLE_WAITING_ROOM_AUTO_REQUEST: boolean; ENABLE_TEXT_TRACKS: boolean; + PLAY_REMOTE_AUDIO: boolean; } declare var $config: ConfigInterface; declare module 'customization' { diff --git a/template/src/pages/test.ts b/template/src/pages/test.ts new file mode 100644 index 000000000..4ec966b88 --- /dev/null +++ b/template/src/pages/test.ts @@ -0,0 +1,4 @@ +export const AudioData = { + audioContent: + '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', +}; diff --git a/template/src/subComponents/caption/useStreamMessageUtils.ts b/template/src/subComponents/caption/useStreamMessageUtils.ts index d720260b7..9dbb50654 100644 --- a/template/src/subComponents/caption/useStreamMessageUtils.ts +++ b/template/src/subComponents/caption/useStreamMessageUtils.ts @@ -1,7 +1,9 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import {useCaption} from './useCaption'; import protoRoot from './proto/ptoto'; import PQueue from 'p-queue'; +import {useLocalUid} from '../../../agora-rn-uikit'; +import {useTextToVoice} from '../../utils/useTextToVoice'; type StreamMessageCallback = (args: [number, Uint8Array]) => void; type FinalListType = { @@ -17,7 +19,9 @@ const useStreamMessageUtils = (): { activeSpeakerRef, prevSpeakerRef, } = useCaption(); + const {textToVoice} = useTextToVoice(); + const localUid = useLocalUid(); let captionStartTime: number = 0; const finalList: FinalListType = {}; const finalTranscriptList: FinalListType = {}; @@ -202,6 +206,17 @@ const useStreamMessageUtils = (): { ? existingStringBuffer + ' ' + latestString : latestString; + if (currentFinalText && textstream.uid !== localUid) { + console.log( + 'debugging new caption text ', + currentFinalText, + ' spoken by ', + textstream.uid, + ); + + textToVoice(currentFinalText); + } + // updating the captions captionText && setCaptionObj(prevState => { diff --git a/template/src/utils/testData.ts b/template/src/utils/testData.ts new file mode 100644 index 000000000..4ec966b88 --- /dev/null +++ b/template/src/utils/testData.ts @@ -0,0 +1,4 @@ +export const AudioData = { + audioContent: + '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', +}; diff --git a/template/src/utils/useTextToVoice.ts b/template/src/utils/useTextToVoice.ts new file mode 100644 index 000000000..da1b62625 --- /dev/null +++ b/template/src/utils/useTextToVoice.ts @@ -0,0 +1,112 @@ +import {Base64} from '../ai-agent/utils'; +import {AudioData} from './testData'; + +const RIMI_API_TOKEN = `Rl8b_9inNeP0l4tOoapYOcl_mjnYig7jmbS-5XGuLlo`; + +export function useTextToVoice() { + function base64ToBinary(base64String) { + const binaryString = Base64.atob(base64String); + return binaryString; + } + + function binaryStringToUint8Array(binaryString) { + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + function createBlobFromUint8Array(uint8Array) { + const blob = new Blob([uint8Array], {type: 'audio/mpeg'}); + return blob; + } + + function createURLFromBlob(blob) { + return URL.createObjectURL(blob); + } + + function playAudio(audioURL) { + const audio = new Audio(audioURL); + audio.play(); + } + + function base64ToMp3(base64String) { + const binaryString = base64ToBinary(base64String); + const uint8Array = binaryStringToUint8Array(binaryString); + const blob = createBlobFromUint8Array(uint8Array); + const audioURL = createURLFromBlob(blob); + return audioURL; + } + + const convertTextToBase64Audio = async text => { + try { + const options = { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${RIMI_API_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + speaker: 'abbie', + text: text, + //default is mist + //modelId: 'mist', + lang: 'eng', + audioFormat: 'mp3', + samplingRate: 22050, + speedAlpha: 1.0, + reduceLatency: false, + }), + }; + const response = await fetch( + 'https://users.rime.ai/v1/rime-tts', + options, + ); + const data = await response.json(); + if (data && data?.audioContent) { + return Promise.resolve(data?.audioContent); + } + } catch (error) { + console.error( + 'Error on useTextToVoice - convertTextToBase64Audio', + error, + ); + return Promise.reject(error); + } + }; + + const textToVoice = async text => { + try { + //api to convert text to base64 audio data + const base64String = await convertTextToBase64Audio(text); + //for testing + //const base64String = AudioData.audioContent; + //base64 to mp3 + const audioURL = base64ToMp3(base64String); + + //Play the audio + playAudio(audioURL); + + return Promise.resolve(audioURL); + } catch (error) { + console.error('Error on useTextToVoice - textToVoice', error); + return Promise.reject(error); + } + }; + + const decodeAndPlayAudio = audioContent => { + try { + //base64 to mp3 + const audioURL = base64ToMp3(audioContent); + //Play the audio + playAudio(audioURL); + } catch (error) { + console.error('Error on useTextToVoice - decodeAndPlayAudio', error); + } + }; + + return {convertTextToBase64Audio, decodeAndPlayAudio, textToVoice}; +} From 6806a10976301f8d1337d89d1f6d4464c22539ed Mon Sep 17 00:00:00 2001 From: Bhupendra Negi Date: Fri, 13 Jun 2025 11:08:21 +0530 Subject: [PATCH 6/7] peoxy server websocket --- .../caption/useStreamMessageUtils.ts | 5 +- template/src/utils/useTextToVoice.ts | 93 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/template/src/subComponents/caption/useStreamMessageUtils.ts b/template/src/subComponents/caption/useStreamMessageUtils.ts index 9dbb50654..086ed401b 100644 --- a/template/src/subComponents/caption/useStreamMessageUtils.ts +++ b/template/src/subComponents/caption/useStreamMessageUtils.ts @@ -19,7 +19,7 @@ const useStreamMessageUtils = (): { activeSpeakerRef, prevSpeakerRef, } = useCaption(); - const {textToVoice} = useTextToVoice(); + const {textToVoice, textToVoice2} = useTextToVoice(); const localUid = useLocalUid(); let captionStartTime: number = 0; @@ -214,7 +214,8 @@ const useStreamMessageUtils = (): { textstream.uid, ); - textToVoice(currentFinalText); + // textToVoice(currentFinalText); + textToVoice2(currentFinalText); } // updating the captions diff --git a/template/src/utils/useTextToVoice.ts b/template/src/utils/useTextToVoice.ts index da1b62625..aa1af1467 100644 --- a/template/src/utils/useTextToVoice.ts +++ b/template/src/utils/useTextToVoice.ts @@ -108,5 +108,96 @@ export function useTextToVoice() { } }; - return {convertTextToBase64Audio, decodeAndPlayAudio, textToVoice}; + const textToVoice3 = async (text: string) => { + try { + const response = await fetch('http://localhost:3001/tts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({text}), + }); + + if (!response.ok) { + throw new Error('Failed to fetch TTS audio'); + } + + const blob = await response.blob(); + const audioURL = URL.createObjectURL(blob); + + const audio = new Audio(audioURL); + audio.play(); + + return Promise.resolve(audioURL); + } catch (error) { + console.error('Error on useTextToVoice - textToVoice (via proxy)', error); + return Promise.reject(error); + } + }; + + const textToVoice2 = async (text: string) => { + try { + const response = await fetch('http://localhost:3001/tts', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({text}), + }); + + if (!response.ok) throw new Error('TTS streaming failed'); + + const mediaSource = new MediaSource(); + const audio = new Audio(); + audio.src = URL.createObjectURL(mediaSource); + audio.play().then(() => { + console.log('[TTS] Audio playback started'); + }); + + mediaSource.addEventListener('sourceopen', () => { + console.log('[TTS] MediaSource opened'); + const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg'); + + const reader = response.body?.getReader(); + const pump = () => { + if (!reader) return; + reader.read().then(({done, value}) => { + if (done) { + console.log('[TTS] All chunks received. Closing stream...'); + if (!sourceBuffer.updating) mediaSource.endOfStream(); + return; + } + if (!value) return; + + console.log( + `[TTS] 🔄 Received audio chunk: ${value.byteLength} bytes`, + ); + + if (!sourceBuffer.updating) { + sourceBuffer.appendBuffer(value); + pump(); + } else { + sourceBuffer.addEventListener( + 'updateend', + () => { + sourceBuffer.appendBuffer(value); + pump(); + }, + {once: true}, + ); + } + }); + }; + + pump(); + }); + } catch (error) { + console.error('[TTS] Error in streaming text-to-voice:', error); + } + }; + + return { + convertTextToBase64Audio, + decodeAndPlayAudio, + textToVoice, + textToVoice2, + }; } From c250ef2476574c3158e8b23fd9c81a3d07f951c2 Mon Sep 17 00:00:00 2001 From: Bhupendra Negi Date: Fri, 13 Jun 2025 11:43:30 +0530 Subject: [PATCH 7/7] poc -rime -rbsocket --- template/src/utils/useTextToVoice.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/template/src/utils/useTextToVoice.ts b/template/src/utils/useTextToVoice.ts index aa1af1467..d9096ad3c 100644 --- a/template/src/utils/useTextToVoice.ts +++ b/template/src/utils/useTextToVoice.ts @@ -137,11 +137,14 @@ export function useTextToVoice() { const textToVoice2 = async (text: string) => { try { - const response = await fetch('http://localhost:3001/tts', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({text}), - }); + const response = await fetch( + 'https://rime-tts-proxy-production.up.railway.app/tts', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({text}), + }, + ); if (!response.ok) throw new Error('TTS streaming failed');