Listing voice designer samples + List Component cleanup

This commit is contained in:
Victor committed 2023-11-22 19:18:59 -08:00
1 parent 4684f566ef
commit 46fe2a481f
19 files changed
+253 -212

No files matched your search

+1
View File
@@ -36,6 +36,7 @@
},
"dependencies": {
"@react-spring/types": "^9.7.3",
"moment": "^2.29.4",
"universal-cookie": "^6.1.1",
"yarn": "^1.22.19"
},
@@ -1,15 +1,15 @@
import MakeRequest from "../../MakeRequest";
export interface DreateVoiceRequest {
export interface DeleteSampleRequest {
set_delete: boolean;
as_mod: boolean;
}
export interface DreateVoiceResponse {
export interface DeleteSampleResponse {
success: boolean
}
export const DeleteDataset = MakeRequest<string, DreateVoiceRequest, DreateVoiceResponse>({
export const DeleteSample = MakeRequest<string, DeleteSampleRequest, DeleteSampleResponse>({
method: "DELETE",
routingFunction: (sampleToken: string) => `/v1/voice_designer/sample/${ sampleToken }/delete`,
});
@@ -3,19 +3,8 @@ import MakeRequest from "../../MakeRequest";
export interface ListSamplesForDatasetRequest {}
export interface ListSamplesForDatasetResponse {
sample_token: string,
media_file_token: string,
media_type: string,
public_bucket_directory_hash: string,
maybe_public_bucket_prefix?: string,
maybe_public_bucket_extension?: string,
maybe_creator_user_token?: string,
created_at: Date,
updated_at: Date,
samples: any,
success: true
}
export const ListSamplesForDataset = MakeRequest<string, ListSamplesForDatasetRequest, ListSamplesForDatasetResponse>({
@@ -0,0 +1,4 @@
.fy-audio-blob {
padding: 1rem;
margin-bottom: 1rem;
}
@@ -0,0 +1,35 @@
import React from "react";
import { a, useTransition } from "@react-spring/web";
import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
import { FileActions } from "components/common";
import "./AudioBlobPreview.scss";
interface Props {
blob?: any,
clear?: any,
file?: any,
hideActions: boolean,
onRest?: () => void,
success?: any,
submit?: () => void,
working?: any
}
export default function AudioBlobPreview({ clear, blob, file, hideActions, onRest, success, submit, working }: Props) {
const transitions = useTransition(file, {
config: { tension: 120, friction: 15 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
onRest,
});
return transitions((style, i) => i ? <a.div {...{ className: "audio-details", style }}>
<div {...{ className: "fy-audio-blob panel" }}>
<InputVcAudioPlayer {...{ filename: blob as string }}/>
</div>
{
!hideActions && <FileActions {...{ clear, success, submit, working }}/>
}
</a.div> : null );
};
@@ -0,0 +1 @@
export { default } from './AudioBlobPreview';
@@ -1,7 +1,7 @@
import React from "react";
import { a, useTransition } from '@react-spring/web';
import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
import { FileActions, FileDetails, FileLabel, FileWrapper } from 'components/common';
// import { a, useTransition } from '@react-spring/web';
// import { InputVcAudioPlayer } from "v2/view/_common/InputVcAudioPlayer";
import { FileDetails, FileLabel, FileWrapper } from 'components/common';
import { faFileAudio } from "@fortawesome/pro-solid-svg-icons";
import './AudioInput.scss'
@@ -9,39 +9,38 @@ const fileTypes = ["MP3", "WAV", "FLAC", "OGG"];
interface Props {
blob?: string;
children?: any;
clear?: (file?: any) => void;
file?: any;
hideActions?: boolean;
hideDetails?: boolean;
hideClearDetails?: boolean;
inputProps?: any;
onRest?: () => void;
success?: boolean;
submit?: () => void;
working?: boolean;
[x:string]: any;
}
const n = () => {};
export default function AudioInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, onRest = n, success = false, submit = n, working = false, ...rest }: Props) {
export default function AudioInput({ children, clear = n, file, hideDetails, hideClearDetails, inputProps, ...rest }: Props) {
const transitions = useTransition(file, {
config: { tension: 120, friction: 15 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
onRest,
});
// const transitions = useTransition(file, {
// config: { tension: 120, friction: 15 },
// from: { opacity: 0 },
// enter: { opacity: 1 },
// leave: { opacity: 0 },
// onRest,
// });
return <div {...{ className: "fy-audio-uploader" }}>
<FileWrapper {...{ fileTypes, ...inputProps, panelClass: "p-3", ...rest }}>
{ file ? <FileDetails {...{ clear, hideClearDetails, icon: faFileAudio, file }}/> : <FileLabel {...{ fileTypes }}/> }
</FileWrapper>
{ transitions((style, i) => i ? <a.div {...{ className: "audio-details", style }}>
<div {...{ className: "panel panel-inner rounded p-3" }}>
<InputVcAudioPlayer {...{ filename: blob as string }}/>
</div>
{ !hideActions && <FileActions {...{ clear, success, submit, working }}/> }
</a.div> : null ) }
{ children
// transitions((style, i) => i ? <a.div {...{ className: "audio-details", style }}>
// <div {...{ className: "panel panel-inner rounded p-3" }}>
// <InputVcAudioPlayer {...{ filename: blob as string }}/>
// </div>
// { !hideActions && <FileActions {...{ clear, success, submit, working }}/> }
// </a.div> : null )
}
</div>;
};
@@ -1,3 +1,4 @@
import AudioBlobPreview from "./AudioBlobPreview";
import AudioInput from "./AudioInput";
import BasicVideo from "./BasicVideo";
import Button from "./Button";
@@ -20,6 +21,7 @@ import TempInput from "./TempInput";
import TempSelect from "./TempSelect";
export {
AudioBlobPreview,
AudioInput,
BasicVideo,
Button,
@@ -28,7 +28,7 @@ interface JobsListProps{
export default function InferenceJobsList({ filter, onSelect, statusTxt, t, inferenceJobs }: JobsListProps) {
// const { inferenceJobs = [] } = useInferenceJobs({ type: 0 });
// const oldJobs = thejobs || inferenceJobs;
console.log("🥬",inferenceJobs);
const [pending, pendingSet] = useState<GetPendingTtsJobCountSuccessResponse>({
success: true,
pending_job_count: 0,
@@ -97,7 +97,6 @@ export default function InferenceJobsList({ filter, onSelect, statusTxt, t, infe
{ jobs
// .filter(filter)
.map((job: any, key: number) => {
console.log("🍇",job);
return <div {...{ className: "panel face-animator-job", key }}>
<FontAwesomeIcon {...{ className: `job-status-icon job-status-${job.statusIndex}`, icon: statusIcons[job.statusIndex] }}/>
<div {...{ className: "job-details" }}>
@@ -12,21 +12,12 @@ interface Props {
export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n }: Props) {
const [file, fileSet] = useState<any>(undefined);
const [blob, blobSet] = useState<string>();
const [status, statusSet] = useState<number>(0);
const inputRef = useRef<HTMLInputElement>(null);
const submit = async () => {
if (file === undefined) { return false; }
statusSet(1);
const onUploadResult = await onSubmit(file);
if (onUploadResult) { statusSet(2); }
else { statusSet(3); }
};
const fileChange = (inputFile?: any) => {
onChange(file);
onChange(inputFile);
fileSet(inputFile || null);
blobSet(inputFile ? URL.createObjectURL(inputFile) : "");
statusSet(0);
};
const inputChange = ({ target = {} }: { target: any }) => {
fileChange(target.value);
@@ -35,7 +26,6 @@ export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n
if (inputRef?.current?.value) inputRef.current.value = '';
fileChange();
onClear();
statusSet(0);
};
return {
@@ -45,10 +35,6 @@ export default function useFile({ debug, onChange = n, onClear = n, onSubmit = n
inputProps: {
onChange: inputChange,
inputRef
},
status,
submit,
success: status === 2,
working: status === 1,
}
};
};
@@ -1,6 +1,6 @@
import React from "react";
import { animated } from "@react-spring/web";
import { AudioInput, BasicVideo, Checkbox, ImageInput, SegmentButtons } from "components/common";
import { AudioBlobPreview, AudioInput, BasicVideo, Checkbox, ImageInput, SegmentButtons } from "components/common";
import { FaceAnimatorSlide } from "./FaceAnimatorTypes";
export default function FaceAnimatorInput({
@@ -35,9 +35,17 @@ export default function FaceAnimatorInput({
onRest: (p: any, c: any, item: any, l: any) => {
toggle.audio(!!audioProps.file);
},
hideActions: true,
// hideActions: true,
}}
/>
>
<AudioBlobPreview {...{
...audioProps,
onRest: (p: any, c: any, item: any, l: any) => {
toggle.audio(!!audioProps.file);
},
hideActions: true,
}}/>
</AudioInput>
<BasicVideo {...{ className: "face-animator-wide-sample", src: "/videos/face-animator-instruction-en.mp4" }}/>
</div>
<div {...{ className: "animation-configure-panel panel" }}>
@@ -79,15 +79,13 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a
? ["Edit Details", "Edit Samples"]
: ["Voice Details", "Upload Samples"];
const key = !isNewCreation && (dataset_token || uuidv4());
// const key = !isNewCreation && (dataset_token || uuidv4());
const displayStep = (step: any) => {
switch (step) {
case 0:
return <VoiceDetails {...{ datasetInputs }} />;
case 1:
return (
<UploadSamples key={key as any} {...{ audioProps, datasetToken: dataset_token, setAudioSamplesReady }} />
);
return <UploadSamples {...{ audioProps, datasetToken: dataset_token, setAudioSamplesReady }}/>;
default:
return null;
}
@@ -9,8 +9,11 @@ import Container from "components/common/Container";
import { NavLink } from "react-router-dom";
import ListItems from "./components/NewList";
import Modal from "components/common/Modal";
import { Button } from "components/common";
import useVoiceRequests from "./useVoiceRequests";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/pro-solid-svg-icons";
import {
FrontendInferenceJobType,
@@ -25,14 +28,9 @@ function VoiceDesignerMainPage({
usePrefixedDocumentTitle("AI Voice Designer");
const { pathname } = useLocation();
const { t } = useLocalize("FaceAnimator");
const { datasets, voices, isFetching } = useVoiceRequests({
requestDatasets: true,
requestVoices: true,
});
const { datasets, voices, isLoading } = useVoiceRequests({ requestDatasets: true,requestVoices: true });
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(
pathname
);
const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf(pathname);
const [deleteItem, setDeleteItem] = useState("");
const [deleteType, setDeleteType] = useState("");
const [deleteText, setDeleteText] = useState({
@@ -76,26 +74,24 @@ function VoiceDesignerMainPage({
history.push(`/voice-designer/voice/${token}`);
};
const voiceClick =
(todo: any, type: string) =>
({ target }: { target: any }) => {
let voiceToken =
voices.list[target.name.split(",")[0].split(":")[1]].voice_token;
todo(voiceToken, type);
};
const DataBadge = () => <span className="dataset-badge mb-0">Dataset</span>;
const VoiceBadge = () => <FontAwesomeIcon icon={faMicrophone} className="me-2 me-lg-3" />;
const datasetClick =
(todo: any, type: string) =>
({ target }: { target: any }) => {
let datasetToken =
datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token;
todo(datasetToken, type);
};
// these need to be abstracted to use over again -V
const voiceClick = (todo: any, type: string) => ({ target }: { target: any }) => {
let voiceToken = voices.list[target.name.split(",")[0].split(":")[1]].voice_token;
todo(voiceToken, type);
};
const datasetClick = (todo: any, type: string) => ({ target }: { target: any }) => {
let datasetToken = datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token;
todo(datasetToken, type);
};
const actionDataSets = datasets.list.map((dataset, i) => {
return {
...dataset,
badge: "dataset",
badge: DataBadge,
buttons: [
{
label: "Edit",
@@ -117,7 +113,7 @@ function VoiceDesignerMainPage({
const actionVoices = voices.list.map((voice, i) => {
return {
...voice,
badge: "voice",
badge: VoiceBadge,
buttons: [
{
label: "Edit",
@@ -157,6 +153,18 @@ function VoiceDesignerMainPage({
to: "/voice-designer/create",
};
const dataPlaceholder = () => <div className="d-flex flex-column list-items p-5 align-items-center">
<h5 className="fw-semibold mb-3">
You haven't created any voices.
</h5>
<Button
icon={faPlus} // 1
label="Create New Voice" // 2
small={true}
to="/voice-designer/create" // 3
/>
</div>;
return (
<>
<Container type="panel">
@@ -204,10 +212,7 @@ function VoiceDesignerMainPage({
</nav>
<div className="p-3 p-lg-4">
<ListItems
{...{ data: view ? actionVoices : actionDataSets }}
isLoading={isFetching}
/>
<ListItems {...{ data: view ? actionVoices : actionDataSets, dataPlaceholder, isLoading }}/>
</div>
</Panel>
</Container>
@@ -88,7 +88,6 @@ function VoiceDesignerVoiceEditPage() {
</div>
<div>
aaaaa
<TempSelect
{...{
icon: faEye,
@@ -27,3 +27,7 @@
font-size: 14px;
}
}
.fy-vd-badge > * {
margin-right: .5rem;
}
@@ -1,124 +1,64 @@
import React from "react";
import Button from "components/common/Button";
import { faMicrophone, faPlus } from "@fortawesome/pro-solid-svg-icons";
import React from 'react';
import Skeleton from "components/common/Skeleton";
import { Button } from "components/common";
import "./ListItems/ListItems.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Skeleton from "components/common/Skeleton";
interface ListItemsProps {
data: any[];
dataPlaceholder?: any;
isLoading: boolean;
}
interface ListItem {
badge: "voice" | "dataset";
badge?: any;
buttons: any;
index: number;
isCreating?: boolean;
name: string;
}
const Working = () => (
<div className="d-flex align-items-center gap-2 py-1">
<p className="fw-medium opacity-75">Voice is being created...</p>
<div className="spinner-border spinner-border-sm text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
const LoaderView = () => <div className="list-items p-3">
<h3 className="mb-0">
<Skeleton type="medium" rounded />
</h3>
</div>;
const ItemRow = ({
badge,
buttons,
index,
isCreating,
name,
...rest
}: ListItem) => {
return (
<div className="d-flex flex-column flex-lg-row gap-3 list-items p-3 align-items-lg-center">
<div className="d-inline-flex flex-wrap align-items-center flex-grow-1">
{badge === "dataset" ? (
<div className="d-flex flex-column flex-lg-row align-items-start gap-2">
<span className="dataset-badge mb-0">Dataset</span>
<h5 className="fw-semibold mb-0">{name}</h5>
</div>
) : null}
{badge === "voice" ? (
<div className="d-inline-flex align-items-center">
<h5 className="fw-semibold d-flex mb-0 align-items-start">
<span>
<FontAwesomeIcon icon={faMicrophone} className="me-2 me-lg-3" />
</span>
{name}
</h5>
</div>
) : null}
</div>
<div className="d-flex">
{isCreating ? (
<Working />
) : (
<div className="d-flex gap-2">
{buttons && buttons.length
? buttons.map((action: any, key: number) => {
return (
<Button
{...{
...action,
key,
name: `item-row:${index},button:${key}`,
}}
/>
);
})
: null}
</div>
)}
</div>
const ItemRow = ({ badge: Badge, buttons, index, isCreating, name, ...rest }: ListItem) => {
return <div className="d-flex flex-column flex-lg-row gap-3 list-items p-3 align-items-lg-center">
<div className="d-inline-flex flex-wrap align-items-center flex-grow-1 gap-2">
<h5 className="fy-vd-badge fw-semibold mb-0">
{ Badge ? <Badge /> : null }
{ name }
</h5>
</div>
);
<div className="d-flex">
<div className="d-flex gap-2">
{buttons && buttons.length
? buttons.map((action: any, key: number) => {
return (
<Button
{...{
...action,
key,
name: `item-row:${index},button:${key}`,
}}
/>
);
})
: null}
</div>
</div>
</div>;
};
export default function NewList({ data, isLoading }: ListItemsProps) {
const DataPlaceholder = () => (
<div className="d-flex flex-column list-items p-5 align-items-center">
<h5 className="fw-semibold mb-3">You haven't created any voices.</h5>
<Button
icon={faPlus} // 1
label="Create New Voice" // 2
small={true}
to="/voice-designer/create" // 3
/>
</div>
);
export default function NewList({ data, dataPlaceholder, isLoading }: ListItemsProps) {
const DataPlaceholder = dataPlaceholder || null;
if (isLoading) {
const loaderView = (
<div className="list-items p-3">
<h3 className="mb-0">
<Skeleton type="medium" rounded />
</h3>
</div>
);
return (
<div className="d-flex flex-column gap-3">
{[...Array(2)].map((_, i) => (
<React.Fragment key={i}>{loaderView}</React.Fragment>
))}
</div>
);
}
return data.length ? (
<div className="d-flex flex-column gap-3">
{data.map((item, key) => (
<ItemRow {...{ key, ...item, index: key }} />
))}
</div>
) : (
<DataPlaceholder />
);
}
return isLoading ? <LoaderView /> :
data.length ? <div className="d-flex flex-column gap-3">
{ data.map((item, key) => <ItemRow {...{ key, ...item, index: key }}/> ) }
</div> :
<DataPlaceholder />;
};
@@ -1,31 +1,94 @@
import React, { useState } from "react";
import VoiceDesignerUploadComponent from "../VoiceDesignerUploadComponent";
import React, { useEffect, useState } from "react";
// import VoiceDesignerUploadComponent from "../VoiceDesignerUploadComponent";
import { AudioInput } from "components/common";
import moment from 'moment';
import ListItems from "../NewList";
import { v4 as uuidv4 } from "uuid";
import { ListSamplesForDataset } from "@storyteller/components/src/api/voice_designer/voice_dataset_samples/ListSamplesForDataset";
import { UploadSample } from "@storyteller/components/src/api/voice_designer/voice_dataset_samples/UploadSample";
import { DeleteSample } from "@storyteller/components/src/api/voice_designer/voice_dataset_samples/DeleteSample";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faWaveform } from "@fortawesome/pro-solid-svg-icons";
function UploadSamples({ audioProps, datasetToken, setAudioSamplesReady }: { audioProps: any, datasetToken?: string, setAudioSamplesReady:any }) {
/* eslint-disable @typescript-eslint/no-unused-vars */
const [convertLoading, setConvertLoading] = useState(false);
const [canConvert, setCanConvert] = useState(false);
const [mediaUploadToken, setMediaUploadToken] = useState<string | undefined>(
undefined
);
const [convertIdempotencyToken, setConvertIdempotencyToken] = useState(
uuidv4()
);
/* eslint-disable @typescript-eslint/no-unused-vars */
// const [convertLoading, setConvertLoading] = useState(false);
// const [canConvert, setCanConvert] = useState(false);
// const [mediaUploadToken, setMediaUploadToken] = useState<string | undefined>();
// const [formIsCleared, setFormIsCleared] = useState(false);
const [uploaded,uploadedSet] = useState(false);
const [samples,samplesSet] = useState<any[]>([]);
const [listFetched,listFetchedSet] = useState(false);
const [formIsCleared, setFormIsCleared] = useState(false);
const SampleBadge = () => <FontAwesomeIcon icon={faWaveform} className="me-2 me-lg-3" />;
const changeConvertIdempotencyToken = () => {
setConvertIdempotencyToken(uuidv4());
const sampleClick = () => ({ target }: { target: any }) => {
let sampleToken = samples[target.name.split(",")[0].split(":")[1]].sample_token;
DeleteSample(sampleToken,{ as_mod: false, set_delete: true })
.then((res) => {
listFetchedSet(false);
// set unBusy here
});
};
const actionSamples = samples.map((sample: any, i: number) => {
let date = new Date(sample.created_at);
return {
...sample,
badge: SampleBadge,
buttons: [
{
label: "Delete",
small: true,
variant: "secondary",
onClick: sampleClick()
},
],
name: `Sample from ${ date ? moment(date).format("MMMM Do YYYY, h:mm a") : "" }`,
};
});
useEffect(() => {
if (audioProps.file && datasetToken && !uploaded) {
uploadedSet(true);
UploadSample("",{
dataset_token: datasetToken || "",
file: audioProps.file,
uuid_idempotency_token: uuidv4(),
})
.then((res) => {
if (res.success) {
audioProps.clear();
uploadedSet(false);
listFetchedSet(false);
}
});
}
if (datasetToken && !listFetched) {
listFetchedSet(true);
ListSamplesForDataset(datasetToken,{})
.then((res) => {
if (res.success && res.samples) {
samplesSet(res.samples);
}
});
}
},[audioProps, datasetToken, listFetched, uploaded]);
return (
<div className="d-flex flex-column gap-4">
<div>
<label className="sub-title">Upload Audio</label>
<div className="d-flex flex-column gap-3 upload-component">
<VoiceDesignerUploadComponent {...{ changeConvertIdempotencyToken, datasetToken, formIsCleared, setCanConvert, setFormIsCleared, setMediaUploadToken, setAudioSamplesReady }}/>
<AudioInput {...{ ...audioProps }}/>
{ samples.length ? <ListItems {...{ data: actionSamples, isLoading: false }}/> : null }
</div>
</div>
</div>
@@ -152,7 +152,6 @@ export default function useVoiceRequests({
}, [user, datasetStatus, voicesStatus]);
return {
isFetching: isBusy(datasetStatus) || isBusy(voicesStatus),
datasets: {
byToken: datasetByToken,
create: createDataset,
@@ -166,6 +165,7 @@ export default function useVoiceRequests({
inference: {
enqueue: EnqueueTts,
},
isLoading: isBusy(datasetStatus) || isBusy(voicesStatus),
languages,
visibilityOptions,
voices: {
+8
View File
@@ -4576,6 +4576,7 @@ __metadata:
craco: ^0.0.3
cross-env: ^7.0.3
lerna: ^4.0.0
moment: ^2.29.4
universal-cookie: ^6.1.1
yarn: ^1.22.19
languageName: unknown
@@ -14797,6 +14798,13 @@ __metadata:
languageName: node
linkType: hard
"moment@npm:^2.29.4":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
languageName: node
linkType: hard
"move-concurrently@npm:^1.0.1":
version: 1.0.1
resolution: "move-concurrently@npm:1.0.1"