Merge pull request #452 from storytold/bflat-fy-video-mocap-page

changes to video mocap page
This commit is contained in:
wil authored and GitHub committed 2024-01-22 09:58:07 -05:00
commit 7a012908ea
6 files changed
+401 -337

No files matched your search

@@ -1,5 +1,5 @@
import React from 'react';
import { a, useSpring } from '@react-spring/web';
import React from "react";
import { a, useSpring } from "@react-spring/web";
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
import "./FileDetails.scss";
@@ -10,51 +10,65 @@ interface Props {
file?: any;
hideClearDetails?: boolean;
icon?: any;
className?: string
className?: string;
}
export default function FileDetails({ className, clear = () => {}, disabled, file, hideClearDetails, icon }: Props) {
export default function FileDetails({
className,
clear = () => {},
disabled,
file,
hideClearDetails,
icon,
}: Props) {
const fileSize =
file && file.size >= 1024 * 1024
? (file.size / 1024 / 1024).toFixed(2) + " MB"
: file
? `${Math.floor(file.size / 1024)} KB`
: null;
? `${Math.floor(file.size / 1024)} KB`
: null;
const style = useSpring({
config: { mass: 1, tension: 120, friction: 14 },
opacity: disabled ? 0 : 1
opacity: disabled ? 0 : 1,
});
return <a.div {...{ className: `fy-uploader-layout upload-details${ className ? " " + className : "" }`, style }}>
{ icon && <Icon {...{ className: "fy-uploader-layout-icon", icon }}/> }
<div>
<div {...{ className: "filename" }}>
{ file.name.slice(0, file.name.lastIndexOf(".")) }
return (
<a.div
{...{
className: `fy-uploader-layout upload-details ${
className ? " " + className : ""
}`,
style,
}}
>
{icon && <Icon {...{ className: "fy-uploader-layout-icon", icon }} />}
<div>
<div {...{ className: "filename d-flex flex-column" }}>
{file.name.slice(0, file.name.lastIndexOf("."))}
<span className="opacity-50">
{`${ file.name.split(".").pop().toUpperCase() } file size: ${ fileSize }`}
</span>
<span className="opacity-50">
{`${file.name
.split(".")
.pop()
.toUpperCase()} file size: ${fileSize}`}
</span>
</div>
</div>
{ !hideClearDetails &&
<button
className="
upload-details-clear
btn btn-destructive
align-items-center
justify-content-center
"
onClick={e=>{
e.preventDefault();
if (clear) clear();
{!hideClearDetails && (
<button
{...{
className:
"upload-details-clear btn btn-destructive align-items-center justify-content-center",
onClick: e => {
e.preventDefault();
clear();
},
}}
>
<Icon icon= {faTrash }/>
</button>
}
</div>
{ !hideClearDetails && <button {...{ className: "upload-details-clear btn btn-destructive align-items-center justify-content-center", onClick: e => { e.preventDefault(); clear() } }}>
<Icon {...{ icon: faTrash }}/>
</button> }
</a.div>;
};
<Icon {...{ icon: faTrash }} />
</button>
)}
</a.div>
);
}
@@ -9,84 +9,88 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons";
interface TabConfig {
label: string;
searcherKey: string;
searcherKey?: string;
weightTypeFilter?: string;
mediaTypeFilter?: string; // NOT DONE
}
interface SelectSearcherProps {
label?: string;
tabs: TabConfig[];
modalTitle?: string;
}
const SelectSearcher = memo(({ label, tabs }: SelectSearcherProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { weightTitle, setWeightTitle } = useToken();
const SelectSearcher = memo(
({ label, tabs, modalTitle = "Select" }: SelectSearcherProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const { weightTitle, setWeightTitle } = useToken();
const openModal = () => {
setIsModalOpen(true);
};
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
const closeModal = () => {
setIsModalOpen(false);
};
const handleRemove = () => {
setWeightTitle && setWeightTitle("");
};
const handleRemove = () => {
setWeightTitle && setWeightTitle("");
};
const searchTabs = tabs.map(tab => ({
label: tab.label,
content: (
<Searcher
type="modal"
onResultSelect={closeModal}
searcherKey={tab.searcherKey}
weightType={tab.weightTypeFilter}
/>
),
padding: true,
}));
const searchTabs = tabs.map(tab => ({
label: tab.label,
content: tab.searcherKey ? (
<Searcher
type="modal"
onResultSelect={closeModal}
searcherKey={tab.searcherKey}
weightType={tab.weightTypeFilter}
/>
) : null,
padding: true,
}));
return (
<>
<div>
{label && <label className="sub-title">{label}</label>}
return (
<>
<div>
{label && <label className="sub-title">{label}</label>}
<div className="d-flex gap-2">
<Input
disabled={true}
className="w-100"
placeholder="None selected"
value={weightTitle ? weightTitle : ""}
/>
<Button
label={weightTitle ? "Change" : "Select"}
onClick={openModal}
/>
{weightTitle && (
<Button
square={true}
variant="danger"
icon={faTrash}
onClick={handleRemove}
tooltip="Remove"
<div className="d-flex gap-2">
<Input
disabled={true}
className="w-100"
placeholder="None selected"
value={weightTitle ? weightTitle : ""}
/>
)}
<Button
label={weightTitle ? "Change" : "Select"}
onClick={openModal}
/>
{weightTitle && (
<Button
square={true}
variant="danger"
icon={faTrash}
onClick={handleRemove}
tooltip="Remove"
/>
)}
</div>
</div>
</div>
<Modal
show={isModalOpen}
handleClose={closeModal}
title="Select a LoRA Weight"
content={() => <NonRouteTabs tabs={searchTabs} />}
showButtons={false}
padding={false}
large={true}
position="top"
mobileFullscreen={true}
/>
</>
);
});
<Modal
show={isModalOpen}
handleClose={closeModal}
title={modalTitle}
content={() => <NonRouteTabs tabs={searchTabs} />}
showButtons={false}
padding={false}
large={true}
position="top"
mobileFullscreen={true}
/>
</>
);
}
);
export default SelectSearcher;
@@ -2,90 +2,122 @@ import React, { useState } from "react";
import { useLocalize } from "hooks";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { PageVideoProvision } from "./components/pageVideoProvision";
// import { PageVideoProvision } from "./components/pageVideoProvision";
import { PageInferenceStatuses } from "./components/pageInferenceStatuses";
import { BasicVideo } from "components/common";
import { BasicVideo, Container, Panel } from "components/common";
import PageHeader from "components/layout/PageHeader";
import Tabs from "components/common/Tabs";
import { Redirect, useLocation } from "react-router-dom";
import TabContentUpload from "./components/tabContentUpload";
import TabContentLibrary from "./components/tabContentLibrary";
export default function VideoMotionCapture(props: {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper,
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void,
inferenceJobs: Array<InferenceJob>,
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>,
}){
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}) {
const { enqueueInferenceJob } = props;
const { t } = useLocalize("VideoMotionCapture");
enum pageStates { VIDEO_PROVISION, SHOW_JOB_STATUS }
const { VIDEO_PROVISION, SHOW_JOB_STATUS} = pageStates;
enum pageStates {
VIDEO_PROVISION,
SHOW_JOB_STATUS,
}
const { VIDEO_PROVISION, SHOW_JOB_STATUS } = pageStates;
const [pageState, setPageState] = useState<{
index:number,
inputMediaToken: string,
jobToken: string,
resultMediaToken: string
}>({ index: VIDEO_PROVISION, inputMediaToken: "", jobToken: "", resultMediaToken: ""});
const handlePageState = (
{tokenType, token }:{tokenType:string, token:string | undefined}
) => {
if(token && tokenType === "jobToken"){
index: number;
inputMediaToken: string;
jobToken: string;
resultMediaToken: string;
}>({
index: VIDEO_PROVISION,
inputMediaToken: "",
jobToken: "",
resultMediaToken: "",
});
const handlePageState = ({
tokenType,
token,
}: {
tokenType: string;
token: string | undefined;
}) => {
if (token && tokenType === "jobToken") {
setPageState({
...pageState,
index: SHOW_JOB_STATUS,
jobToken: token
})
jobToken: token,
});
}
};
const { pathname } = useLocation();
if (pathname === `/video-mocap` || pathname === `/video-mocap/`) {
return <Redirect to={`/video-mocap/upload`} />;
}
const tabs = [
{
label: t("tabTitle.upload"),
content: <TabContentUpload t={t} pageStateCallback={handlePageState} />,
to: "/video-mocap/upload",
padding: true,
},
{
label: t("tabTitle.library"),
content: <TabContentLibrary t={t} pageStateCallback={handlePageState} />,
to: "/video-mocap/select-media",
padding: true,
},
];
return (
<div className="container-panel py-4">
<div className="panel p-4">
{/*Header section*/}
<div className="row g-5">
<h1 className="fw-bold">{t("headings.title")}</h1>
<p className="fa-light-txt opacity-75 mt-1">{t("headings.subtitle")}</p>
</div>
<div className="row g-5 mt-1">
<Container type="panel" className="mb-5">
<PageHeader
title={t("headings.title")}
subText={t("headings.subtitle")}
/>
<Panel>
<div className="row g-0">
{/*Video Provision Tabs & Job Statuses*/}
<div className="col-12 col-md-6">
{ pageState.index === VIDEO_PROVISION &&
<PageVideoProvision
t={t}
pageStateCallback={handlePageState}
/>
}
{ pageState.index === SHOW_JOB_STATUS &&
{pageState.index === VIDEO_PROVISION && <Tabs tabs={tabs} />}
{pageState.index === SHOW_JOB_STATUS && (
<PageInferenceStatuses
{...{
t,
enqueueInferenceJob,
pageStates,
pageState,
pageStateCallback: handlePageState
pageStateCallback: handlePageState,
}}
/>
}
)}
</div>
{/*ENDS Video Chooser Tabs*/}
<div className="col-12 col-md-6">
<BasicVideo
title="Video -> Mocap Sample"
src="/videos/face-animator-instruction-en.mp4"
/>
<Panel padding={true} clear={true}>
<BasicVideo
title="Video -> Mocap Sample"
src="/videos/face-animator-instruction-en.mp4"
/>
</Panel>
</div>
</div>{/*2nd row*/}
</div>{/*panel*/}
</div>
)
</div>
{/*2nd row*/}
</Panel>
{/*panel*/}
</Container>
);
}
@@ -1,83 +1,89 @@
import React, { useEffect } from "react";
import { useSpring } from '@react-spring/web';
import { useSpring } from "@react-spring/web";
import { FileDetails, FileWrapper, FileLabel } from "components/common";
import { faFileVideo } from "@fortawesome/pro-solid-svg-icons";
import { faFileVideo, faVideo } from "@fortawesome/pro-solid-svg-icons";
import "./style.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface Props {
//blob, clear, file, inputProps comes from file
blob?: string,
clear?: (file?: any) => void,
file?: any,
hideActions?: boolean,
hideClearDetails?: boolean,
inputProps?: any,
onStateChange?: () => void,
success?: boolean,
submit?: () => void,
working?: boolean,
t: Function,
[x:string]: any,
blob?: string;
clear?: (file?: any) => void;
file?: any;
hideActions?: boolean;
hideClearDetails?: boolean;
inputProps?: any;
onStateChange?: () => void;
success?: boolean;
submit?: () => void;
working?: boolean;
t: Function;
[x: string]: any;
}
export default function UploadFieldVideo({
blob = "",
clear = ()=>{},
clear = () => {},
file,
hideActions,
hideClearDetails,
inputProps,
onStateChange = ()=>{},
onStateChange = () => {},
success = false,
submit = ()=>{},
submit = () => {},
working = false,
t,
...rest }: Props)
{
useEffect(()=>{
...rest
}: Props) {
useEffect(() => {
//only fire for file unload
//on file load is fired in video elements
if (!file) onStateChange();
}, [file, onStateChange])
}, [file, onStateChange]);
const style = useSpring({
config: { mass: 1, tension: 120, friction: 14 }
config: { mass: 1, tension: 120, friction: 14 },
});
const fileTypes = ["MP4"];
return <FileWrapper {...{ fileTypes, panelClass: "ratio-1x1", ...inputProps, ...rest }}>
<div className="d-flex justify-content-center align-items-center w-100 h-100 overflow-hidden">
{ file ?
<>
<FileDetails
className="
position-absolute top-0
return (
<FileWrapper {...{ fileTypes, panelClass: "", ...inputProps, ...rest }}>
<div className="d-flex flex-column justify-content-center align-items-center w-100 h-100 overflow-hidden">
{file ? (
<>
<FileDetails
className="d-flex w-100 p-3"
{...{ clear, file, hideClearDetails, icon: faFileVideo }}
/>
<video
controls
src={blob}
className="mh-100 mw-100 object-fit-cover"
onLoadStart={onStateChange}
{...{ style }}
/>
</>
) : (
<>
<FileLabel
className="
upload-details
d-flex w-100 p-3
"
{...{ clear, file, hideClearDetails, icon: faFileVideo }}
/>
<video
controls
src={blob}
className="mh-100 mw-100 object-fit-cover"
onLoadStart={onStateChange}
{...{style}}
/>
</> :
<>
<FileLabel className="
upload-details
position-absolute top-0
d-flex w-100 p-3
"
{...{ fileTypes }}
/>
{/* this is the fontAwesome faVideo */}
<svg className="fill-secondary" xmlns="http://www.w3.org/2000/svg" height="160" width="200" viewBox="0 0 576 512"><path d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z"/></svg>
</>
}
</div>
</FileWrapper>;
};
{...{ fileTypes }}
/>
<div className="ratio ratio-16x9">
<div className="d-flex align-items-center justify-content-center">
<FontAwesomeIcon
icon={faVideo}
className="opacity-25 display-1"
/>
</div>
</div>
</>
)}
</div>
</FileWrapper>
);
}
@@ -1,31 +1,28 @@
import SelectSearcher from "components/common/SelectSearcher/SelectSearcher";
import React from "react";
export default function TabContentLibrary(props: {
t: Function,
pageStateCallback: Function
}){
const { t } = props
return(
<div
className="tab-pane fade py-4"
id="vmcLibrary"
>
<div className="row">
<div className="col-12">
<div className="d-flex gap-3">
<div className="flex-grow-1">
<input className="form-control w-100" placeholder={t("input.filenamePlaceholder")} />
</div>
<button className="btn btn-primary">{t("button.select")}</button>
</div>
</div>
<div className="col-12 py-4">
<div className="d-flex justify-content-end">
<button className="btn btn-primary" disabled>{t("button.generate")}</button>
</div>
</div>
</div>
t: Function;
pageStateCallback: Function;
}) {
return (
<div>
<SelectSearcher
modalTitle="Select a Video"
label="Select a Video"
tabs={[
{
label: "All Videos",
searcherKey: "allVideos",
mediaTypeFilter: "",
},
{
label: "Bookmarked",
searcherKey: "bookmarkedVideos",
mediaTypeFilter: "",
},
]}
/>
</div>
)
}
);
}
@@ -1,24 +1,30 @@
import React, { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { UploadMedia } from '@storyteller/components/src/api/media_files/UploadMedia'
import {
EnqueueVideoMotionCapture
} from "@storyteller/components/src/api/video_mocap";
import { UploadMedia } from "@storyteller/components/src/api/media_files/UploadMedia";
import { EnqueueVideoMotionCapture } from "@storyteller/components/src/api/video_mocap";
import { useFile } from "hooks";
import VideoInput from "v2/view/pages/video_mocap/components/VideoInput";
export default function TabContentUpload(props:{
t: Function
pageStateCallback: (data:{tokenType:string, token:string | undefined}) => void
}){
const { t, pageStateCallback } = props
const videoProps = useFile({})
export default function TabContentUpload(props: {
t: Function;
pageStateCallback: (data: {
tokenType: string;
token: string | undefined;
}) => void;
}) {
const { t, pageStateCallback } = props;
const videoProps = useFile({});
enum tabStates {
NO_FILE, FILE_STAGED, FILE_UPLOADING, FILE_UPLOADED, MOCAPNET
NO_FILE,
FILE_STAGED,
FILE_UPLOADING,
FILE_UPLOADED,
MOCAPNET,
}
const { NO_FILE, FILE_STAGED, FILE_UPLOADING, FILE_UPLOADED, MOCAPNET } = tabStates;
const { NO_FILE, FILE_STAGED, FILE_UPLOADING, FILE_UPLOADED, MOCAPNET } =
tabStates;
const [tabState, setTabState] = useState<number>(NO_FILE);
const [token, setToken] = useState<string>("");
@@ -26,105 +32,110 @@ export default function TabContentUpload(props:{
uuid_idempotency_token: uuidv4(),
file: videoProps.file,
source: "file",
type:"video",
type: "video",
});
const handleUploadVideo = ()=>{
setTabState(FILE_UPLOADING)
UploadMedia(makeRequest()).then((res=>{
if(res.success && "media_file_token" in res){
setToken(res.media_file_token)
const handleUploadVideo = () => {
setTabState(FILE_UPLOADING);
UploadMedia(makeRequest()).then(res => {
if (res.success && "media_file_token" in res) {
setToken(res.media_file_token);
}
}));
}
useEffect(()=>{
if (token!=="") setTabState(FILE_UPLOADED)
}, [token, FILE_UPLOADED])
});
};
useEffect(() => {
if (token !== "") setTabState(FILE_UPLOADED);
}, [token, FILE_UPLOADED]);
const handleEnqueueMocapNet = ()=>{
const handleEnqueueMocapNet = () => {
const request = {
video_source: token,
uuid_idempotency_token: uuidv4()
}
EnqueueVideoMotionCapture(request)
.then(res=>{
pageStateCallback({
tokenType: "jobToken",
token: res.inference_job_token
})
uuid_idempotency_token: uuidv4(),
};
EnqueueVideoMotionCapture(request).then(res => {
pageStateCallback({
tokenType: "jobToken",
token: res.inference_job_token,
});
setTabState(MOCAPNET)
}
});
setTabState(MOCAPNET);
};
// contains upload inout state and controls, see docs
if (tabState === NO_FILE || tabState === FILE_STAGED){
return(
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<VideoInput {...{t, ...videoProps,
onStateChange: () => setTabState(videoProps.file ? FILE_STAGED : NO_FILE),
}}/>
</div>
</div>
<div className="row py-3">
<div className="col-12">
<div className="d-flex justify-content-end gap-3">
<button
className="btn btn-primary"
disabled={tabState!==FILE_STAGED}
onClick={handleUploadVideo}
>{t("button.upload")}</button>
</div>
</div>
</div>
</div>
)
} else if (tabState === FILE_UPLOADING){
return(
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.fileUploading")}</h2>
</div>
</div>
</div>
)
}else if (tabState === FILE_UPLOADED){
return(
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.fileUploaded")}</h2>
</div>
<div className="col-12">
<button
className="btn btn-primary"
onClick={handleEnqueueMocapNet}
>{t("button.generate")}</button>
</div>
</div>
</div>
)
} else if (tabState === MOCAPNET){
return(
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.mocapNetRequesting")}</h2>
</div>
</div>
</div>
)
}else {
return(
<div className="tab-pane fade show active py-4" id="vmcUpload">
if (tabState === NO_FILE || tabState === FILE_STAGED) {
return (
<div>
<div className="row">
<div className="col-12">
<h1>{t("message.UnknownError")}</h1>
<div className="col-12">
<VideoInput
{...{
t,
...videoProps,
onStateChange: () =>
setTabState(videoProps.file ? FILE_STAGED : NO_FILE),
}}
/>
</div>
</div>
<div className="row py-3">
<div className="col-12">
<div className="d-flex justify-content-end gap-3">
<button
className="btn btn-primary"
disabled={tabState !== FILE_STAGED}
onClick={handleUploadVideo}
>
{t("button.upload")}
</button>
</div>
</div>
</div>
</div>
)
);
} else if (tabState === FILE_UPLOADING) {
return (
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.fileUploading")}</h2>
</div>
</div>
</div>
);
} else if (tabState === FILE_UPLOADED) {
return (
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.fileUploaded")}</h2>
</div>
<div className="col-12">
<button className="btn btn-primary" onClick={handleEnqueueMocapNet}>
{t("button.generate")}
</button>
</div>
</div>
</div>
);
} else if (tabState === MOCAPNET) {
return (
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h2>{t("tab.message.mocapNetRequesting")}</h2>
</div>
</div>
</div>
);
} else {
return (
<div className="tab-pane fade show active py-4" id="vmcUpload">
<div className="row">
<div className="col-12">
<h1>{t("message.UnknownError")}</h1>
</div>
</div>
</div>
);
}
}
}