Merge pull request #200 from storytold/feature/audio-sidepanel-v2

Feature/audio sidepanel v2
This commit is contained in:
ArEnSc authored and GitHub committed 2024-04-22 20:15:31 -04:00
commit 04df8b73c4
17 files changed
+813 -150

No files matched your search

+4 -1
View File
@@ -11,4 +11,7 @@ export const inferTts = `${apiHost}/tts/inference`;
export const listTts = `${apiHost}/tts/list`;
// User Media
export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`;
export const listMediaByUser = (username:string) => `${apiHost}/v1/media_files/list/user/${username}`;
// Any Media
export const getMediaFileByToken = (fileToken:string) => `${apiHost}/v1/media_files/file/${fileToken}`;
+18
View File
@@ -0,0 +1,18 @@
import { twMerge } from "tailwind-merge"
export const Pill = ({
className,
children,
}:{
className?: string,
children : React.ReactNode
})=>{
return (
<div className={twMerge(
"rounded-md text-xs font-bold bg-brand-primary py-0.5 px-1.5 w-fit",
className
)}>
{children}
</div>
)
}
+2
View File
@@ -12,6 +12,7 @@ import { ListDropdown } from "./ListDropdown"
import { ListSearchDropdown } from "./ListSearchDropdown"
import { LoadingBar } from "./LoadingBar";
import { LoadingDots, LoadingDotsBricks, LoadingDotsTyping } from "./LoadingDots";
import { Pill } from "./Pill";
export {
Button,
@@ -30,4 +31,5 @@ export {
LoadingDots,
LoadingDotsBricks,
LoadingDotsTyping,
Pill
};
@@ -1,41 +1,29 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
import { useSignals, useSignalEffect } from "@preact/signals-react/runtime";
import { faCirclePlus, faArrowsRotate } from "@fortawesome/pro-solid-svg-icons";
import { AppUiContext } from "~/contexts/AppUiContext";
import { AuthenticationContext } from "~/contexts/Authentication";
import { APPUI_ACTION_TYPES } from "~/reducers";
import { audioFilter, audioItems } from "~/pages/PageEnigma/store";
import { AssetFilterOption } from "~/pages/PageEnigma/models";
import { Button, ButtonIcon } from "~/components";
import { MediaItem, AssetType } from "~/pages/PageEnigma/models";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer";
import { ListAudioByUser } from "./listAudioByUser";
import { ListAudioByUser } from "./utilities";
import { inferenceJobs } from "~/pages/PageEnigma/store/inferenceJobs";
import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager";
import { PageLibrary } from "./pageLibrary";
import { PageTTS } from "./pageTTS";
import { AudioTabPages } from "./types";
export const AudioTabComponent = () => {
useSignals();
const [ state, setState ] = useState({
firstLoad: false,
fetchingUserAudio: false,
page: AudioTabPages.LIBRARY,
});
const { authState } = useContext(AuthenticationContext);
const [, dispatchAppUiState] = useContext(AppUiContext);
const handleOpenTtsDialogue = ()=>{
dispatchAppUiState({
type: APPUI_ACTION_TYPES.OPEN_DIALOGUE_TTS
})
}
const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value];
const handleListAudioByUser = useCallback((username:string)=>{
const handleListAudioByUser = useCallback((username:string, sessionToken:string)=>{
setState((curr)=>({...curr, fetchingUserAudio:true}));
ListAudioByUser(username).then((res:any[])=>{
ListAudioByUser(username, sessionToken).then((res:any[])=>{
setState((curr)=>({...curr, fetchingUserAudio:false}));
audioItemsFromServer.value = res.map(item=>{
const morphedItem:MediaItem = {
@@ -44,6 +32,8 @@ export const AudioTabComponent = () => {
media_id: item.token,
object_uuid: item.token,
name: item.maybe_title || item.origin.maybe_model.title,
description: item.maybe_text_transcript,
publicBucketPath: item.public_bucket_path,
length: 25,
thumbnail: "/resources/placeholders/audio_placeholder.png",
isMine: true,
@@ -55,9 +45,9 @@ export const AudioTabComponent = () => {
}, []);
useEffect(()=>{
if ( authState.userInfo ){
if (authState.userInfo && authState.sessionToken){
if(state.firstLoad === false && audioItemsFromServer.value.length === 0){
handleListAudioByUser(authState.userInfo.username);
handleListAudioByUser(authState.userInfo.username, authState.sessionToken);
setState((curr)=>({...curr, firstLoad:true}));
}
}
@@ -82,80 +72,30 @@ export const AudioTabComponent = () => {
return foundItemOfJob !== undefined;
}
});
if(found === undefined){
handleListAudioByUser(authState.userInfo.username);
if(found === undefined && authState.sessionToken){
handleListAudioByUser(authState.userInfo.username, authState.sessionToken);
}
}
});
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start items-center gap-2 px-4">
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.MINE)}
disabled={!allAudioItems.some((item) => item.isMine)}
>
My Audios
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.BOOKMARKED)}
disabled={!allAudioItems.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
<ButtonIcon
className={twMerge(
"absolute right-0 mr-4",
// state.fetchingUserAudio ? "animate-spin" : "",
)}
icon={faArrowsRotate}
onClick={()=>{
if(authState.userInfo?.username){
handleListAudioByUser(authState.userInfo?.username);
}
}}
/>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
onClick={handleOpenTtsDialogue}
>
Generate Audio
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<ItemElements
items={allAudioItems}
assetFilter={audioFilter.value}
/>
</div>
</>
);
const changePage = (newPage:AudioTabPages)=>{
setState((curr)=>({
...curr,
page: newPage
}))
}
if(state.page === AudioTabPages.LIBRARY){
return <PageLibrary changePage={changePage}/>
}else if(state.page === AudioTabPages.TTS && authState.sessionToken){
return(
<PageTTS
changePage={changePage}
sessionToken={authState.sessionToken}
/>
);
}else{
return(
<p>Unknown Error</p>
)
}
};
@@ -0,0 +1,54 @@
import { useCallback, useEffect, useState, useRef } from 'react';
import WaveSurfer from 'wavesurfer.js';
import { faCirclePlay, faCirclePause, } from "@fortawesome/pro-solid-svg-icons"
import { ButtonIcon } from '~/components';
export const WaveformPlayer = ({
audio
}:{
audio: string
}) => {
const waveSurferRef = useRef<WaveSurfer | undefined>(undefined);
const [isPlaying, toggleIsPlaying] = useState(false);
const containerRef = useCallback((node:HTMLDivElement)=>{
if(node){
const waveSurfer = WaveSurfer.create({
container: node,
barWidth: 2,
height: 30,
cursorWidth: 0,
});
waveSurfer.load(audio);
waveSurfer.on('ready', () => {
waveSurferRef.current = waveSurfer;
});
waveSurfer.on('play', () => {
toggleIsPlaying(true);
});
waveSurfer.on('pause', () => {
toggleIsPlaying(false);
});
}
}, []);
useEffect(()=>{
return()=>{
console.log("OK?");
waveSurferRef.current?.destroy();
}
},[]);
return (
<div className="flex py-1 gap-2">
<ButtonIcon
icon={isPlaying ? faCirclePause : faCirclePlay}
size="2x"
onClick={() => {
waveSurferRef.current?.playPause();
}}
/>
<div ref={containerRef} className="w-full h-10"/>
</div>
);
};
@@ -0,0 +1,99 @@
import { useCallback, useContext, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { WaveformPlayer } from "./WaveformPlayer";
import { MediaItem } from "~/pages/PageEnigma/models";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import {
canDrop,
currPosition,
dragItem,
initPosition,
} from "~/pages/PageEnigma/store";
import { H4, H6, Pill } from "~/components";
function getGcsUrl(bucketRelativePath: string | undefined | null): string {
let bucket = "vocodes-public";
let path = bucketRelativePath;
if (path !== undefined && path !== null && !path.startsWith("/")) {
path = "/" + path;
}
return `https://storage.googleapis.com/${bucket}${path}`;
}
interface Props {
item: MediaItem;
}
export const AudioItemElement = ({ item }: Props) => {
useSignals();
const { startDrag, endDrag } = useContext(TrackContext);
const { initX, initY } = initPosition.value;
useEffect(() => {
const onPointerUp = () => {
if (dragItem.value) {
endDrag();
}
};
const onMouseMove = (event: MouseEvent) => {
if (dragItem.value) {
event.stopPropagation();
event.preventDefault();
const deltaX = event.pageX - initX;
const deltaY = event.pageY - initY;
currPosition.value = { currX: initX + deltaX, currY: initY + deltaY };
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [startDrag, endDrag, initX, initY]);
const onPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
startDrag(item);
currPosition.value = {
currX: event.pageX,
currY: event.pageY,
};
initPosition.value = {
initX: event.pageX,
initY: event.pageY,
};
canDrop.value = false;
}
},
[item, startDrag],
);
return (
<div
className="relative w-full cursor-pointer rounded-lg transition-all duration-200"
onPointerDown={onPointerDown}
>
<div
className="flex flex-col p-2 gap-1 w-full rounded-lg bg-assets-background"
>
<div className="flex justify-between pb-1">
<Pill>Audio</Pill>
</div>
{item.publicBucketPath && <WaveformPlayer audio={getGcsUrl(item.publicBucketPath)} />}
<H4 className="text-overflow-ellipsis">{item.name}</H4>
{item.description && <H6 className="text-overflow-ellipsis">{item.description}</H6>}
</div>
</div>
);
};
@@ -0,0 +1,28 @@
import { AudioItemElement } from "./audioItemElement";
import { AssetFilterOption, MediaItem } from "~/pages/PageEnigma/models";
interface Props {
items: MediaItem[];
assetFilter: AssetFilterOption;
}
export const AudioItemElements = ({ items, assetFilter }: Props) => {
const displayItems = items.filter((item) => {
if (assetFilter === AssetFilterOption.ALL) {
return true;
}
if (assetFilter === AssetFilterOption.MINE) {
return item.isMine;
}
return item.isBookmarked;
});
return (
<div className={`grid grid-cols-1 gap-3`}>
{displayItems.map((item) => (
<AudioItemElement key={item.media_id} item={item} />
))}
</div>
);
};
@@ -1,55 +0,0 @@
import { listMediaByUser } from "~/api";
import {
MediaFileType,
MediaFileClass,
MediaFileSubtype,
UserDetailsLight,
WeightCategory,
WeightType
} from "./types";
export interface MediaFile {
token: string;
media_type: MediaFileType;
media_class: MediaFileClass | null;
maybe_media_subtype: MediaFileSubtype | null;
public_bucket_path: string;
maybe_engine_extension: string | null;
maybe_batch_token: string;
maybe_title: string | null;
maybe_original_filename: string | null;
maybe_creator_user: UserDetailsLight | null;
maybe_prompt_token: string | null;
creator_set_visibility: string;
created_at: Date;
updated_at: Date;
maybe_model_weight_info: {
title: string;
weight_token: string;
weight_category: WeightCategory;
weight_type: WeightType;
maybe_weight_creator: UserDetailsLight;
maybe_cover_image_public_bucket_path: string;
};
}
export const ListAudioByUser = async(username:string) => {
return await fetch(listMediaByUser(username),{
method: 'GET',
headers: {
"Accept": "application/json",
},
// credentials: 'include'
})
.then(res => res.json())
.then(res => {
if(res.success && res.results){
return res.results.filter((item:MediaFile)=>item['media_type']==='audio');
}else{
Promise.reject();
}
})
.catch(e => ({ success : false }));
}
@@ -0,0 +1,79 @@
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { useSignals } from "@preact/signals-react/runtime";
import { audioFilter, audioItems } from "~/pages/PageEnigma/store";
import { AssetFilterOption } from "~/pages/PageEnigma/models";
import { audioItemsFromServer } from "~/pages/PageEnigma/store/mediaFromServer";
import { Button } from "~/components";
import { AudioItemElements } from "./audioItemElements";
import { AudioTabPages } from "./types";
export const PageLibrary = ({
changePage
}:{
changePage: (newPage:AudioTabPages) => void
})=>{
useSignals();
const allAudioItems = [...audioItems.value, ...audioItemsFromServer.value];
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start items-center gap-2 px-4">
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.MINE)}
disabled={!allAudioItems.some((item) => item.isMine)}
>
My Audios
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.BOOKMARKED)}
disabled={!allAudioItems.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
onClick={()=>changePage(AudioTabPages.TTS)}
>
Generate Audio
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<AudioItemElements
items={allAudioItems}
assetFilter={audioFilter.value}
/>
</div>
</>
);
}
@@ -0,0 +1,233 @@
import { useCallback, useEffect, useState } from "react";
import { useSignalEffect } from "@preact/signals-react/runtime";
import { v4 as uuidv4 } from "uuid";
import {
faChevronLeft,
faBrainCircuit,
} from "@fortawesome/pro-solid-svg-icons";
import {
Button, ButtonIcon, H2, Label,
LoadingDotsTyping,
ListSearchDropdown,
Textarea
} from "~/components";
import { TtsModelListItem, GenerateTtsAudioResponse } from "~/pages/PageEnigma/models/tts";
import { addInferenceJob, inferenceJobs} from "../../../store/inferenceJobs";
import { JobState } from "~/hooks/useInferenceJobManager/useInferenceJobManager";
import { ListTtsModels, GenerateTtsAudio, GetMediaFileByToken } from "./utilities";
import { AudioTabPages } from "./types";
import { AudioItemElement } from "./audioItemElement";
import { MediaItem, AssetType } from "~/pages/PageEnigma/models";
type TtsState = {
voice: TtsModelListItem | undefined;
text: string;
hasEnqueued :boolean;
inferenceToken?: string;
inferenceJobType?: string;
hasTtsResult: boolean;
}
const initialState:TtsState =
{
voice:undefined,
text:"",
hasEnqueued:false,
inferenceJobType: undefined,
inferenceToken: undefined,
hasTtsResult:false,
};
export const PageTTS =({
changePage,
sessionToken,
}:{
changePage: (newPage:AudioTabPages) => void;
sessionToken: string;
})=>{
const [ttsState, setTtsState] = useState<TtsState>(initialState);
const [ttsResultFile, setTtsResultFile] = useState<MediaItem|undefined>();
useSignalEffect(()=>{
console.log(inferenceJobs.value);
if(ttsState.hasEnqueued && ttsState.inferenceToken){
const found = inferenceJobs.value.find((job)=>job.job_id===ttsState.inferenceToken);
console.log(`finding: ${ttsState.inferenceToken}`);
console.log(found);
if(found?.job_status === JobState.COMPLETE_SUCCESS){
setTtsState((curr)=>({
...curr,
hasTtsResult: true,
}))
GetMediaFileByToken(found.result.entity_token, sessionToken)
.then(res=>{
console.log(res);
const morphedItem:MediaItem = {
version: 1,
type: AssetType.AUDIO,
media_id: res.media_file.token,
object_uuid: res.media_file.token,
name: ttsState.voice?.title || '',
description: ttsState.text,
publicBucketPath: res.media_file.public_bucket_path,
length: 25,
thumbnail: "/resources/placeholders/audio_placeholder.png",
isMine: true,
// isBookmarked?: boolean;
}
setTtsResultFile(morphedItem);
})
}
}
});
const [ttsModels, setTtsModels] = useState<Array<TtsModelListItem>>([]);
const listModels = useCallback(async () => {
const ttsModelsLoaded = ttsModels.length > 0;
if (ttsModelsLoaded) {
return; // Already queried.
}
const models = await ListTtsModels(sessionToken);
if (models) {
setTtsModels(models);
}
}, []);
useEffect(() => {
listModels();
}, [listModels]);
useEffect(()=> {
if(ttsState.hasEnqueued && ttsState.inferenceToken && ttsState.inferenceJobType){
console.log(`tts has Enqueued`);
addInferenceJob({
version:1,
job_id: ttsState.inferenceToken,
job_type: ttsState.inferenceJobType,
job_status: JobState.PENDING,
})
}
},[ttsState]);
const requestTts = useCallback( (sessionToken:string)=>{
const modelToken = ttsState.voice ? ttsState.voice.model_token : undefined;
if(modelToken){
setTtsState((curr)=>({
...curr,
hasEnqueued: true,
inferenceToken: undefined,
inferenceJobType: undefined,
hasAudio: false,
result: undefined,
}));
const request = {
uuid_idempotency_token: uuidv4(),
tts_model_token: modelToken,
inference_text: ttsState.text,
};
GenerateTtsAudio(request, sessionToken).then((
res:GenerateTtsAudioResponse)=>{
if(res && res.inference_job_token){
setTtsState((curr)=>({
...curr,
inferenceToken: res.inference_job_token,
inferenceJobType: res.inference_job_token_type
}));
}
});
}else{
console.log("no voice model selected");
}
},[ttsState]);
const handleTextInput = (
e: React.ChangeEvent<HTMLTextAreaElement>
)=>{
setTtsState((curr)=>({
...curr,
text: e.target.value,
}));
};
const handleOnSelect = (val:string)=>{
console.log( val);
const voiceModel = ttsModels.find((item)=>{
if (item.title === val) return item
})
setTtsState((curr)=>({
...curr,
voice: voiceModel,
}));
}
return(
<div className="flex flex-col p-4">
<div className="flex items-center">
<ButtonIcon
icon={faChevronLeft}
onClick={()=>changePage(AudioTabPages.LIBRARY)}
/>
<H2>Generate TTS</H2>
</div>
<Label className="mb-1">Select a Voice</Label>
{ttsModels.length > 0 && <ListSearchDropdown
list={ttsModels}
listDisplayKey="title"
onSelect={handleOnSelect}
/> }
<div className="flex w-full justify-between mt-4">
<Label>What would you like to say?</Label>
<div className="flex gap-2 items-center">
</div>
</div>
<Textarea
placeholder="Enter what you want the voice to say here."
value={ttsState.text}
onChange={handleTextInput}
/>
<div className="mt-6 flex gap-2">
<div className="w-full h-12">
{!ttsState.hasTtsResult && !ttsState.hasEnqueued &&
<Button
className="w-36 h-full text-xl "
variant={ttsState.hasTtsResult ? "secondary" : "primary" }
disabled={ttsState.text === ""}
icon={faBrainCircuit}
onClick={()=>requestTts(sessionToken)}
>
Generate
</Button>
}
{!ttsState.hasTtsResult && ttsState.hasEnqueued &&
<LoadingDotsTyping className="bg-brand-secondary-500 rounded-lg"/>
}
{ ttsResultFile &&
<AudioItemElement item={ttsResultFile}/>
}
</div>
</div>
<div className="mt-6 flex justify-between gap-2">
{ ttsState.hasEnqueued &&
<Button
type="button"
disabled={!ttsState.hasTtsResult}
onClick={()=>requestTts(sessionToken)}
icon={faBrainCircuit}
>
Generate Another
</Button>
}
</div>
</div>
);
}
@@ -122,3 +122,39 @@ export enum WeightType {
VALL_E = "vall_e",
NONE = "none",
}
export enum AudioTabPages {
LIBRARY = 'library',
TTS = 'tts',
}
export interface MediaFile {
token: string;
media_type: MediaFileType;
media_class: MediaFileClass | null;
maybe_media_subtype: MediaFileSubtype | null;
public_bucket_path: string;
maybe_engine_extension: string | null;
maybe_batch_token: string;
maybe_title: string | null;
maybe_original_filename: string | null;
maybe_creator_user: UserDetailsLight | null;
maybe_prompt_token: string | null;
creator_set_visibility: string;
created_at: Date;
updated_at: Date;
maybe_model_weight_info: {
title: string;
weight_token: string;
weight_category: WeightCategory;
weight_type: WeightType;
maybe_weight_creator: UserDetailsLight;
maybe_cover_image_public_bucket_path: string;
};
}
export interface GetMediaFileResponse {
success: boolean;
media_file?: MediaFile;
}
@@ -0,0 +1,138 @@
import { listTts, inferTts, listMediaByUser, getMediaFileByToken } from '~/api';
import { MediaFile, GetMediaFileResponse } from './types';
import {
TtsModelListItem,
TtsModelListResponsePayload,
GenerateTtsAudioErrorType,
StatusLike,
GenerateTtsAudioRequest,
GenerateTtsAudioResponse,
} from '~/pages/PageEnigma/models/tts';
export const ListAudioByUser = async(username:string, sessionToken: string) => {
return await fetch(listMediaByUser(username),{
method: 'GET',
headers: {
"Accept": "application/json",
'session': sessionToken,
},
// credentials: 'include'
})
.then(res => res.json())
.then(res => {
if(res.success && res.results){
return res.results.filter((item:MediaFile)=>item['media_type']==='audio');
}else{
Promise.reject();
}
})
.catch(e => ({ success : false }));
}
export async function ListTtsModels(sessionToken:string) : Promise<Array<TtsModelListItem>| undefined> {
return await fetch(listTts, {
method: 'GET',
headers: {
'Accept': 'application/json',
'session': sessionToken,
},
// credentials: 'include',
})
.then(res => res.json())
.then(res => {
const response : TtsModelListResponsePayload = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(e => {
return undefined;
});
}
export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorType | undefined {
switch (statuslike.status) {
case 400:
return GenerateTtsAudioErrorType.BadRequest;
case 404:
return GenerateTtsAudioErrorType.NotFound;
case 429:
return GenerateTtsAudioErrorType.TooManyRequests;
case 500:
return GenerateTtsAudioErrorType.ServerError;
}
}
// export function GenerateTtsAudioIsError(response: GenerateTtsAudioResponse): response is GenerateTtsAudioError {
// return !('inference_job_token' in response);
// }
export async function GenerateTtsAudio(request: GenerateTtsAudioRequest, sessionToken:string) : Promise<GenerateTtsAudioResponse>
{
return await fetch(inferTts, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
},
// credentials: 'include',
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!('inference_job_token' in res)) {
return { error: GenerateTtsAudioErrorType.UnknownError };
}
const ret:GenerateTtsAudioResponse = {
success: true,
inference_job_token: res.inference_job_token,
inference_job_token_type: res.inference_job_token_type,
}
return ret;
})
.catch(e => {
let maybeError = maybeMapError(e);
if (maybeError !== undefined) {
return { error: maybeError };
}
return { error: GenerateTtsAudioErrorType.UnknownError };
});
};
export async function GetMediaFileByToken (fileToken: string, sessionToken: string) : Promise<GetMediaResponse>
{
return await fetch(getMediaFileByToken(fileToken), {
method: "GET",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
},
})
.then(res => res.json())
.then(res => {
const response: GetMediaFileResponse = res;
if (response && response.success && response.media_file) {
// NB: Timestamps aren't converted to Date objects on their own!
response.media_file.created_at = new Date(
response.media_file.created_at
);
response.media_file.updated_at = new Date(
response.media_file.updated_at
);
return response;
} else {
return { success: false };
}
})
.catch(e => {
return { success: false };
});
;
}
+2
View File
@@ -4,6 +4,8 @@ export interface MediaItem {
media_id: string;
object_uuid?: string;
name: string;
description?: string;
publicBucketPath?: string;
length?: number;
thumbnail?: string;
isMine?: boolean;
+77
View File
@@ -0,0 +1,77 @@
/*
* Types for Listing TTS Models
*/
export interface TtsModelListResponsePayload {
success: boolean,
models: Array<TtsModelListItem>,
}
export interface UserRatings {
positive_count: number,
negative_count: number,
// Total count does not take into account "neutral" ratings.
total_count: number,
}
export interface TtsModelListItem {
model_token: string,
tts_model_type: string,
creator_user_token: string,
creator_username: string,
creator_display_name: string,
creator_gravatar_hash: string,
ietf_language_tag: string,
ietf_primary_language_subtag: string,
updatable_slug: string,
title: string,
is_front_page_featured: boolean,
is_twitch_featured: boolean,
user_ratings: UserRatings,
category_tokens: string[],
created_at: string,
updated_at: string,
}
/*
* Types for Making a TTS Generation Request
*/
export interface GenerateTtsAudioRequest {
uuid_idempotency_token: string,
tts_model_token: string,
inference_text: string,
// TODO(2022-03): TEMPORARY
is_storyteller_demo?: boolean,
}
export interface GenerateTtsAudioResponse {
success?: boolean,
inference_job_token?: string,
// Which queue to poll
inference_job_token_type?: string,
error?: GenerateTtsAudioErrorType,
}
export enum GenerateTtsAudioErrorType {
BadRequest,
NotFound,
TooManyRequests,
ServerError,
UnknownError,
}
export interface EndpointSuccessResponse {
success: boolean,
inference_job_token: string,
inference_job_token_type?: string,
}
export interface EndpointErrorResponse {
success: boolean,
}
export type EndpointResponse = EndpointSuccessResponse | EndpointErrorResponse;
export interface StatusLike {
status: number,
}
+2
View File
@@ -455,6 +455,7 @@ export const audioItems = signal<MediaItem[]>([
length: 25,
name: "Talk (Demo Sounds)",
thumbnail: "/resources/placeholders/audio_placeholder.png",
publicBucketPath: "/media/j/a/r/r/3/jarr3asge6t0x048wdzcehzjd2nh4ep7/fakeyou_jarr3asge6t0x048wdzcehzjd2nh4ep7.wav",
},
{
version: 1,
@@ -463,6 +464,7 @@ export const audioItems = signal<MediaItem[]>([
length: 25,
name: "NCS Song",
thumbnail: "/resources/placeholders/audio_placeholder.png",
publicBucketPath: "/media/s/j/t/0/6/sjt06a8y2qrdqe574nry02bpd3bt01ma/upload_sjt06a8y2qrdqe574nry02bpd3bt01ma.wav",
},
]);
+6
View File
@@ -32,6 +32,7 @@
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"wavesurfer.js": "^7.7.11",
"webm-writer": "^1.0.0"
},
"devDependencies": {
@@ -13181,6 +13182,11 @@
"fsevents": "~2.3.2"
}
},
"node_modules/wavesurfer.js": {
"version": "7.7.11",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.7.11.tgz",
"integrity": "sha512-27otJlDBcUSbMh/YkxEZN0yXTHINQNYSOl5aJplYlIaHw2u3BrABUzOmUZV3dKIy8udOLJXS/67y5pOIzgAg+w=="
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+1
View File
@@ -39,6 +39,7 @@
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"wavesurfer.js": "^7.7.11",
"webm-writer": "^1.0.0"
},
"devDependencies": {