From fd93adb9bd48460920c9bbb16ffab3dff1865617 Mon Sep 17 00:00:00 2001 From: DJDavid98 Date: Sun, 17 Dec 2023 04:49:13 +0100 Subject: [PATCH] Add TTS health indicator to Chat --- src/js/chat/Chat.tsx | 5 +++ src/js/chat/TtsHealth.tsx | 61 ++++++++++++++++++++++++++ src/js/hooks/use-tts.ts | 6 ++- src/js/model/removable-element-id.ts | 8 ++++ src/scss/modules/TtsHealth.module.scss | 43 ++++++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/js/chat/TtsHealth.tsx create mode 100644 src/scss/modules/TtsHealth.module.scss diff --git a/src/js/chat/Chat.tsx b/src/js/chat/Chat.tsx index ac52824..6b55a6c 100644 --- a/src/js/chat/Chat.tsx +++ b/src/js/chat/Chat.tsx @@ -16,6 +16,9 @@ import DurationUnitFormat from 'intl-unofficial-duration-unit-format'; import { useSettings } from '../contexts/settings-context'; import { SettingName } from '../model/settings'; import { useTts } from '../hooks/use-tts'; +import { TtsHealth } from './TtsHealth'; +import { RemovableElement } from '../RemovableElement'; +import { RemovableElementId } from '../model/removable-element-id'; const MAX_MESSAGE_COUNT = 12; @@ -199,6 +202,8 @@ export const Chat: FC = () => { }, [addMessage, chatSongPreviews, df, socket, tts]); return + {ttsEnabled && elevenLabsToken && + } {messages.map(message => )} ; }; diff --git a/src/js/chat/TtsHealth.tsx b/src/js/chat/TtsHealth.tsx new file mode 100644 index 0000000..09c9175 --- /dev/null +++ b/src/js/chat/TtsHealth.tsx @@ -0,0 +1,61 @@ +import { FC, useId, useMemo } from 'react'; +import useSWR from 'swr'; +import * as styles from '../../scss/modules/TtsHealth.module.scss'; + +export const ELEVEN_LABS_SUBSCRIPTION_ENDPOINT = 'https://api.elevenlabs.io/v1/user/subscription'; + +export interface TtsHealthProps { + token: string; +} + +export const TtsHealth: FC = ({ token }) => { + const pf = useMemo(() => new Intl.NumberFormat('en-US', { + style: 'percent', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }), []); + const nf = useMemo(() => new Intl.NumberFormat('en-US'), []); + const progressBarId = useId(); + + const { data: subscriptionData } = useSWR(ELEVEN_LABS_SUBSCRIPTION_ENDPOINT, (key: string) => fetch(key, { + method: 'GET', + headers: { + 'xi-api-key': token, + }, + }).then(r => r.json()), { + refreshInterval: 60e3, + revalidateOnFocus: false, + keepPreviousData: true, + }); + + const limits = useMemo(() => { + let maxChars = 0; + let usedChars = 0; + if (typeof subscriptionData === 'object' && subscriptionData !== null) { + if ('character_limit' in subscriptionData && typeof subscriptionData.character_limit === 'number') { + maxChars = subscriptionData.character_limit; + } + if ('character_count' in subscriptionData && typeof subscriptionData.character_count === 'number') { + usedChars = subscriptionData.character_count; + } + } + return { maxChars, usedChars }; + }, [subscriptionData]); + + if (limits.maxChars === 0) return null; + + const charsAvailable = limits.maxChars - limits.usedChars; + + return
+ + +
; +}; diff --git a/src/js/hooks/use-tts.ts b/src/js/hooks/use-tts.ts index 8efd5bb..7b8a274 100644 --- a/src/js/hooks/use-tts.ts +++ b/src/js/hooks/use-tts.ts @@ -6,6 +6,8 @@ import { ttsNameSubstitutions, VoiceGender } from '../utils/chat-messages'; +import { useSWRConfig } from 'swr'; +import { ELEVEN_LABS_SUBSCRIPTION_ENDPOINT } from '../chat/TtsHealth'; interface TtsInput { id?: string; @@ -32,6 +34,7 @@ export const useTts = (token: string | null, enabled: boolean | null): TtsApi => return age === 'young' && gender === targetGender && useCase === 'narration'; }); }, []); + const { mutate } = useSWRConfig(); const clearPlayingAudio = useCallback(() => { if (audioPlayerRef.current) { @@ -87,6 +90,7 @@ export const useTts = (token: string | null, enabled: boolean | null): TtsApi => body: JSON.stringify({ text: textToRead }) }); const audioBlob = await response.blob(); + void mutate(ELEVEN_LABS_SUBSCRIPTION_ENDPOINT); if (!audioPlayerRef.current) { audioPlayerRef.current = new Audio(); @@ -100,7 +104,7 @@ export const useTts = (token: string | null, enabled: boolean | null): TtsApi => processQueue('ended handler').then(resolve); }); }); - }, [enabled, token, getVoice, clearPlayingAudio]); + }, [enabled, token, getVoice, mutate, clearPlayingAudio]); const readText = useCallback(async (text: TtsInput) => { if (!enabled) return; diff --git a/src/js/model/removable-element-id.ts b/src/js/model/removable-element-id.ts index 7acf62c..fe2a827 100644 --- a/src/js/model/removable-element-id.ts +++ b/src/js/model/removable-element-id.ts @@ -8,6 +8,7 @@ export enum RemovableElementId { BEAT_SABER = 'beat-saber-root', BOUNCY = 'bouncy-root', CHAT = 'chat-root', + TTS_HEALTH = 'tts-health-root', CONNECTION = 'connection-root', CHANNEL_BUG = 'credits-root', HEART_RATE = 'heart-rate-root', @@ -72,6 +73,9 @@ export const elementsTree: RemovableElementsTree = { [RemovableElementId.CHAT]: { name: 'Chat Overlay', description: 'Shows incoming chat messages based on the provided configuration', + children: [ + RemovableElementId.TTS_HEALTH + ], }, [RemovableElementId.HEART_RATE]: { name: 'Heart Rate', @@ -88,6 +92,10 @@ export const elementsTree: RemovableElementsTree = { name: 'Channel Bug', description: 'Also known as "digital on-screen graphic", a dynamically changing element that cycles between various states' }, + [RemovableElementId.TTS_HEALTH]: { + name: 'TTS Health Bar', + description: 'Shows the percentage of available TTS characters in the current subscription tier' + }, }; export const isRemovableElementId = (value?: string): value is RemovableElementId => value !== undefined && value in elementsTree; diff --git a/src/scss/modules/TtsHealth.module.scss b/src/scss/modules/TtsHealth.module.scss new file mode 100644 index 0000000..0d6fc80 --- /dev/null +++ b/src/scss/modules/TtsHealth.module.scss @@ -0,0 +1,43 @@ +.tts-health { + position: absolute; + top: 0; + left: 1rem; + right: 0; + z-index: 10; + box-sizing: border-box; + color: rgba(255, 0, 0, .75); + background-color: rgb(40, 0, 0); + padding: .5em; + border-radius: .4em; + font-size: .6em; + width: calc(100% - 2em); + + .tts-label { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + + .tts-name { + font-weight: bold; + } + + .tts-percent { + text-align: right; + } + } + + progress { + height: .2em; + border-radius: .2em; + background-color: #202020; + width: 100%; + border: .125em solid #202020; + display: block; + + + &::-moz-progress-bar, &::-webkit-progress-bar { + background-color: currentColor; + border-radius: inherit; + } + } +}