Merge branch 'feature/mvp' into feature/comp/playbar-w-trim

This commit is contained in:
wil committed 2024-03-13 10:24:42 -04:00
commit 24fdf2845e
24 files changed
+228 -145

No files matched your search

@@ -16,6 +16,10 @@ export enum FrontendInferenceJobType {
VideoStyleTransfer,
}
export enum AllInferenceJobs { All }
export type JobListOptions = FrontendInferenceJobType | AllInferenceJobs;
// NB: Many of these fields are optional despite the response payload containing them
// This is because we create temporary placeholder objects with just the token.
export class InferenceJob {
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -26,6 +26,12 @@ interface Scene3DProps {
// CSS classes to apply to the engine
className?: string;
// Callback when the scene is first loaded
// Not always 100% true. Sometimes it prematurely fires.
onSceneReadyCallback?: () => void;
// Callback called when the scene is saved.
// It will receive the argument of the new scene media token.
onSceneSavedCallback?: (mediaToken: string) => void;
}
@@ -35,6 +41,7 @@ export default function Scene3D({
skybox,
fullScreen = false,
className,
onSceneReadyCallback,
onSceneSavedCallback,
}: Scene3DProps) {
@@ -54,6 +61,10 @@ export default function Scene3D({
console.log("studio-ready message (2)");
if (onSceneReadyCallback !== undefined) {
onSceneReadyCallback();
}
// NB: Example of how to call the API in the other direction:
// studio.postMessage("save-scene", engineBaseUrl);
} else if (
@@ -70,7 +81,7 @@ export default function Scene3D({
} else if (event.data === "scene-save-failed") {
console.error("Failed to save the scene!");
}
}, [onSceneSavedCallback]);
}, [onSceneReadyCallback, onSceneSavedCallback]);
useEffect(() => {
@@ -129,6 +129,7 @@
bottom: 0;
left: 0;
font-size: 14px;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
}
&::after {
@@ -140,14 +141,12 @@
height: 50%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
z-index: 2;
height: 35%;
}
&.selected::after {
background: linear-gradient(
to top,
rgba(desaturate(darken($primary, 20%), 30%), 70%),
transparent
);
background: linear-gradient(to top, rgba(#632323, 90%), transparent);
height: 35%;
}
}
@@ -3,7 +3,6 @@ import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/
// import { useTransition } from "@react-spring/web";
import JobItem from "./JobItem";
import { useInferenceJobs, useLocalize, useSession } from "hooks";
import { JobListTypes } from "hooks/useInferenceJobs/useInferenceJobs";
import "./InferenceJobsList.scss";
import { Button, Panel, JobQueueTicker } from "components/common";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -12,7 +11,6 @@ import { faClipboardList } from "@fortawesome/pro-solid-svg-icons";
interface JobsListProps {
failures: (fail: string) => string;
jobType?: FrontendInferenceJobType;
value?: JobListTypes;
onSelect?: (e: any) => any;
panel?: boolean;
showJobQueue?: boolean;
@@ -34,21 +32,15 @@ const resultPaths = {
export default function InferenceJobsList({
failures,
jobType,
value,
onSelect,
panel = true,
showHeader = true,
showJobQueue = false,
showNoJobs = false
}: JobsListProps) {
const jobValue = value !== undefined ? value : jobType !== undefined ? (jobType || 0) + 1 : 0;
// undefined specified here to allow 0.
// jobType + 1 because the difference between FrontendInferenceJobType and JobListTypes is an "all" option
const { sessionSubscriptions } = useSession();
const hasPaidFeatures = sessionSubscriptions?.hasPaidFeatures();
const { inferenceJobs = [], jobStatusDescription } = useInferenceJobs(jobValue);
const { inferenceJobs = [], jobStatusDescription } = useInferenceJobs(jobType);
const { t } = useLocalize("InferenceJobs");
const jobContent = (
@@ -1,10 +1,10 @@
import React from "react"; // useState
import React, { useState } from "react"; // useState
import InferenceJobsList from "components/layout/InferenceJobsList";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
// import { TempSelect as Select } from "components/common";
// import { enumToKeyArr } from "resources";
import { TempSelect as Select } from "components/common";
import { enumToKeyArr } from "resources";
import ModalHeader from "../ModalHeader";
import { useLocalize } from "hooks";
import { useLocalize, useSession } from "hooks";
interface Props {
handleClose?: any;
@@ -18,18 +18,17 @@ export default function InferenceJobsModal({
showModalHeader = true,
...rest
}: Props) {
// const presetFilter = enumToKeyArr(FrontendInferenceJobType)[inJobType];
// const [jobType,jobTypeSet] = useState(inJobType > -1 ? presetFilter : "All");
// const typeObj = ["All", ...Object.values(FrontendInferenceJobType)];
// const options = typeObj.filter(val => isNaN(Number(val)))
// .map((value) => {
// if (typeof value === "string") return { value, label: value }
// return { label: "", value: "" };
// });
const { canAccessStudio } = useSession();
const presetFilter = enumToKeyArr(FrontendInferenceJobType)[inJobType];
const [jobType,jobTypeSet] = useState(inJobType > -1 ? presetFilter : "All");
const typeObj = ["All", ...Object.values(FrontendInferenceJobType)];
const options = typeObj.filter(val => isNaN(Number(val)))
.map((value) => {
if (typeof value === "string") return { value, label: value }
return { label: "", value: "" };
});
const { t } = useLocalize("InferenceJobs");
const selectedType = typeObj.indexOf(jobType) - 1;
const failures = (fail = "") => {
switch (fail) {
default:
@@ -40,8 +39,8 @@ export default function InferenceJobsModal({
return (
<>
{ showModalHeader && <ModalHeader {...{ handleClose, title: t("core.jobsTitle") }} /> }
{
// <Select {...{ onChange: ({ target }: { target: any }) => jobTypeSet(target.value), options, value: jobType }} />
{ canAccessStudio() &&
<Select {...{ onChange: ({ target }: { target: any }) => jobTypeSet(target.value), options, value: jobType }} />
}
<InferenceJobsList
{...{
@@ -49,8 +48,7 @@ export default function InferenceJobsModal({
onSelect: () => {
if (handleClose) handleClose();
},
value: 0, // fixed for now
// value: typeObj.indexOf(jobType),
...selectedType > -1 ? { jobType: selectedType } : {},
showHeader: false,
showJobQueue: true,
showNoJobs: true,
@@ -6,6 +6,5 @@ interface Props {
export default function ServerStatusChecker({ value }: Props) {
useStatusPoll();
console.log("🤬",);
return null;
};
@@ -1,19 +1,70 @@
import React, { useState } from "react";
import { SessionContext } from "context";
import React, { createContext, useState } from "react";
// import { SessionContext } from "context";
import { Modal } from "components/common";
import AccountModal from "components/layout/AccountModal";
import { ModalView } from "context/SessionContext";
// import { ModalView } from "context/SessionContext";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
import { StudioRolloutHostnameAllowed } from "@storyteller/components/src/utils/StudioRolloutHostnameAllowed";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
export enum ModalView { // ignore this modal stuff for now -V
Closed,
Signup,
Login,
}
interface ModalProps { // this too
close: () => void;
open: () => void;
view: ModalView;
}
interface SessionContextType {
canAccessStudio: () => boolean;
canEditTtsModel: (token: string) => boolean;
canBanUsers: () => boolean;
check: () => boolean;
loggedIn: boolean;
modal: ModalProps;
querySession?: any;
querySubscriptions?: any;
sessionFetched: boolean;
sessionSubscriptions?: any;
studioAccessCheck: (x:any) => any,
user?: any;
userTokenMatch: (token: string) => boolean;
}
interface Props {
children?: any;
querySession: any;
querySubscriptions: any;
querySession: () => void;
querySubscriptions: () => void;
sessionFetched: boolean;
sessionSubscriptions: any;
sessionWrapper?: any;
sessionSubscriptions: SessionSubscriptionsWrapper;
sessionWrapper: SessionWrapper;
}
// Functions are initially No-ops/dummies so that they are never undefined and never have to be called conditionally.
// These functions will never actually fire because they are immediately redefined in the provider below,
// as they must be as they utilize the provider's state.
export const SessionContext = createContext<SessionContextType>({
canAccessStudio: () => false,
canEditTtsModel: () => false,
canBanUsers: () => false,
check: () => false,
loggedIn: false,
sessionFetched: false,
studioAccessCheck: () => null,
modal: {
close: () => {},
open: () => {},
view: ModalView.Closed,
},
userTokenMatch: () => false,
});
export default function SessionProvider({
children,
querySession,
@@ -22,19 +73,12 @@ export default function SessionProvider({
sessionSubscriptions,
sessionWrapper,
}: Props) {
const sessionResponse = sessionWrapper?.sessionStateResponse || {
logged_in: false,
};
const sessionResponse = sessionWrapper?.sessionStateResponse || { logged_in: false, user: null };
const { logged_in: loggedIn, user } = sessionResponse;
const [view, viewSet] = useState(ModalView.Closed);
const open = () => viewSet(ModalView.Signup);
const close = () => {
viewSet(ModalView.Closed);
};
const viewSwitch = () =>
view === ModalView.Signup
? viewSet(ModalView.Login)
: viewSet(ModalView.Signup);
const close = () => { viewSet(ModalView.Closed); };
const viewSwitch = () => viewSet(view === ModalView.Signup ? ModalView.Login : ModalView.Signup);
const check = () => {
if (user) {
return true;
@@ -43,23 +87,31 @@ export default function SessionProvider({
return false;
}
};
const userTokenMatch = (otherUserToken: string) =>
!otherUserToken || !user?.user_token ? false : user.user_token === otherUserToken;
const canEditTtsModel = (userToken: string) =>
user?.canEditOtherUsersTtsModels || userTokenMatch(userToken);
user?.can_delete_other_users_tts_models || userTokenMatch(userToken);
const canBanUsers = () => user?.can_ban_users || false;
const studioAccessCheck = (content: React.ElementType) => !sessionWrapper?.canAccessStudio() ? <StudioNotAvailable /> : content;
const canAccessStudio = () => {
const hostnameAllowed = StudioRolloutHostnameAllowed();
const userAllowed = !!user?.can_access_studio || !!user?.maybe_feature_flags.includes("studio");
return hostnameAllowed && userAllowed;
}
const studioAccessCheck = (content: React.ElementType) => canAccessStudio() ? content : <StudioNotAvailable />;
const modal = { close, open, view };
return (
<SessionContext.Provider
{...{
value: {
canAccessStudio,
canEditTtsModel,
canBanUsers,
check,
loggedIn,
modal: { close, open, view },
modal,
querySession,
querySubscriptions,
sessionFetched,
@@ -1 +1 @@
export { default } from "./SessionProvider";
export { default, SessionContext } from "./SessionProvider";
@@ -5,11 +5,7 @@ import { InferenceJobsContext } from 'context';
import { useModal } from "hooks";
import { InferenceJobsModal } from "components/modals";
export enum JobListAll { All }
export type JobListTypes = FrontendInferenceJobType | JobListAll;
export default function useInferenceJobs(jobType: JobListTypes = 0, debug = false) {
export default function useInferenceJobs(jobType?: FrontendInferenceJobType, debug = false) {
const { byCategory, enqueue, inferenceJobs, queueStats } = useContext(InferenceJobsContext);
const { open } = useModal();
const openJobListModal = () => open({ component: InferenceJobsModal, props: { jobType } });
@@ -20,7 +16,8 @@ export default function useInferenceJobs(jobType: JobListTypes = 0, debug = fals
if (!noModalPls) { openJobListModal(); }
enqueue(jobToken,jobType);
},
inferenceJobs: jobType === JobListAll.All ? inferenceJobs : (byCategory?.get(jobType - 1) || []),
inferenceJobs: jobType === undefined ? inferenceJobs : (byCategory?.get(jobType) || []),
// inferenceJobs: jobType === AllInferenceJobs.All ? inferenceJobs : (byCategory?.get(jobType) || []),
jobStatusDescription: (jobState: JobState) => Object.keys(JobState).filter(key => isNaN(Number(key)))[jobState],
queueStats
};
@@ -1,5 +1,5 @@
import { useContext } from 'react';
import { SessionContext } from 'context';
import { useContext } from "react";
import { SessionContext } from "components/providers/SessionProvider";
export default function useInferenceJobs() {
return useContext(SessionContext);
@@ -20,9 +20,7 @@ interface Props {
export default function EngineCompositor({ value, sessionWrapper }: Props) {
const [mediaToken, mediaTokenSet] = useState();
const onChange = ({ target }: any) => mediaTokenSet(target.value);
const inferenceJobs = useInferenceJobs(
FrontendInferenceJobType.EngineComposition
);
const inferenceJobs = useInferenceJobs(FrontendInferenceJobType.EngineComposition);
const onClick = () => {
EnqueueEngineCompositing("", {
@@ -43,7 +43,6 @@ export default function FbxToGltfPage({
const { inferenceJobs } = useInferenceJobs(
FrontendInferenceJobType.ConvertFbxtoGltf
);
const hasConversionJobs = inferenceJobs && inferenceJobs.length > 0;
console.log("inferenceJobs", inferenceJobs);
@@ -151,11 +150,9 @@ export default function FbxToGltfPage({
imageUrl="/images/header/fbx-to-gltf.png"
/>
{hasConversionJobs && (
<div className="mb-4">
<FbxToGltfJobList />
</div>
)}
<Panel padding={true}>
<div className="d-flex flex-column gap-3">
@@ -43,6 +43,8 @@ function LandingPage(props: Props) {
const isLoggedIn = props.sessionWrapper.isLoggedIn();
const canAccessStudio = props.sessionWrapper.canAccessStudio();
return (
<>
{domain.website === Website.StorytellerAi && !isLoggedIn && (
@@ -79,7 +81,7 @@ function LandingPage(props: Props) {
{/* STORYTELLER,AI */}
{!isLoggedIn ? (
<>
<OnboardingSelection />
{!canAccessStudio && <OnboardingSelection />}
<VstSectionV2 />
<TtsDemoSection
sessionWrapper={props.sessionWrapper}
@@ -1,5 +1,5 @@
import { Badge, Panel } from "components/common";
import React, { useEffect, useRef, useState } from "react";
import React, { useState } from "react";
import "./VstSectionV2.scss";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
@@ -22,39 +22,40 @@ interface VstSectionV2Props {}
export default function VstSectionV2(props: VstSectionV2Props) {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
const [activeIndex, setActiveIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(true);
// const [isPlaying, setIsPlaying] = useState(true);
// const togglePlayback = () => {
// setIsPlaying(!isPlaying);
// };
const handleVideoPlay = () => {
if (!isPlaying) {
setIsPlaying(true);
}
};
// const handleVideoPlay = () => {
// if (!isPlaying) {
// setIsPlaying(true);
// }
// };
const handleVideoPause = () => {
if (isPlaying) {
setIsPlaying(false);
}
};
// const handleVideoPause = () => {
// if (isPlaying) {
// setIsPlaying(false);
// }
// };
const videoRef = useRef<HTMLVideoElement | null>(null);
// const videoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
}
}, [isPlaying]);
// useEffect(() => {
// if (videoRef.current) {
// if (isPlaying) {
// videoRef.current.play();
// } else {
// videoRef.current.pause();
// }
// }
// }, [isPlaying]);
const videoSet = [
{
src: "/videos/landing/vst/1-original.mp4",
thumbnail: "/videos/landing/vst/1-original.jpg",
styledVideos: [
{
src: "/videos/landing/vst/1-2d_flat_anime.mp4",
@@ -80,6 +81,7 @@ export default function VstSectionV2(props: VstSectionV2Props) {
},
{
src: "/videos/landing/vst/2-original.mp4",
thumbnail: "/videos/landing/vst/2-original.jpg",
styledVideos: [
{
src: "/videos/landing/vst/2-2d_flat_anime.mp4",
@@ -157,16 +159,17 @@ export default function VstSectionV2(props: VstSectionV2Props) {
muted
playsInline
controls={false}
preload="auto"
key={`${index}-${activeIndex}`}
onPlay={handleVideoPlay}
onPause={handleVideoPause}
ref={videoRef => {
if (videoRef && isPlaying) {
videoRef.play();
} else if (videoRef) {
videoRef.pause();
}
}}
// onPlay={handleVideoPlay}
// onPause={handleVideoPause}
// ref={videoRef => {
// if (videoRef && isPlaying) {
// videoRef.play();
// } else if (videoRef) {
// videoRef.pause();
// }
// }}
>
<source src={video.src} type="video/mp4" />
Your browser does not support the video tag.
@@ -186,10 +189,10 @@ export default function VstSectionV2(props: VstSectionV2Props) {
{videoSet.map((video, index) => (
<SwiperSlide key={index}>
<div className="ratio ratio-16x9 vst-carousel-panel">
<video loop muted playsInline controls={false}>
<source src={video.src} type="video/mp4" />
Your browser does not support the video tag.
</video>
<img
src={video.thumbnail}
alt="Demo Original Video Thumbnail"
/>
</div>
</SwiperSlide>
))}
@@ -218,16 +221,17 @@ export default function VstSectionV2(props: VstSectionV2Props) {
muted
playsInline
controls={false}
preload="auto"
key={styledVideo.src}
onPlay={handleVideoPlay}
onPause={handleVideoPause}
ref={videoRef => {
if (videoRef && isPlaying) {
videoRef.play();
} else if (videoRef) {
videoRef.pause();
}
}}
// onPlay={handleVideoPlay}
// onPause={handleVideoPause}
// ref={videoRef => {
// if (videoRef && isPlaying) {
// videoRef.play();
// } else if (videoRef) {
// videoRef.pause();
// }
// }}
>
<source src={styledVideo.src} type="video/mp4" />
Your browser does not support the video tag.
@@ -10,31 +10,31 @@ export default function OnboardingSelection(props: OnboardingSelectionProps) {
const cards = [
{
text: "Scene 1",
backgroundImage: "",
text: "Room Scene",
backgroundImage: "/images/landing/onboarding/room-scene-thumbnail.webp",
onClick: () => {
history.push("/studio-intro/");
history.push("/studio-intro/m_ejhs95fc5aybp36h4a79k7523ds6an");
},
},
{
text: "Scene 2",
backgroundImage: "",
onClick: () => {
history.push("/studio-intro/");
history.push("/studio-intro");
},
},
{
text: "Scene 3",
backgroundImage: "",
onClick: () => {
history.push("/studio-intro/");
history.push("/studio-intro");
},
},
{
text: "Scene 4",
backgroundImage: "",
onClick: () => {
history.push("/studio-intro/");
history.push("/studio-intro");
},
},
];
@@ -19,7 +19,6 @@ interface Props {
}
function StudioIntroPage(props: Props) {
const [camera,cameraSet] = useState("rotation");
const { mediaToken } = useParams<{ mediaToken: string }>();
const history = useHistory();
@@ -32,11 +31,28 @@ function StudioIntroPage(props: Props) {
// for subsequent steps of this flow.
const [savedMediaToken, setSavedMediaToken] = useState(mediaToken);
// We don't show the "next step" buttons until the engine loads.
// Unfortunately the engine sometimes fires this twice, with one instance
// being called before the scene loads. Until this is fixed, we'll count the
// number of event fires and assume we must have two calls.
const [sceneIsLoadedCount, sceneIsLoadedCountSet] = useState(0);
// If the scene is saved, we know the user must have interacted.
// This can serve as a second optional gate for enabling the next steps.
const [sceneIsSaved, sceneIsSavedSet] = useState(false);
const [camera, cameraSet] = useState("rotation");
usePrefixedDocumentTitle("Storyteller Studio");
const onSaveCallback = useCallback((sceneMediaToken: string) => {
setSavedMediaToken(sceneMediaToken);
}, [setSavedMediaToken]);
sceneIsSavedSet(true); // Just in case we missed the "scene loaded" event.
}, [setSavedMediaToken, sceneIsSavedSet]);
const onSceneReadyCallback = useCallback(() => {
sceneIsLoadedCountSet(sceneIsLoadedCount + 1);
}, [sceneIsLoadedCount, sceneIsLoadedCountSet])
if (!props.sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />;
@@ -84,6 +100,30 @@ function StudioIntroPage(props: Props) {
value: "static"
}];
const sceneIsLoaded = sceneIsLoadedCount > 1 || sceneIsSaved;
let progressButtons = <></>;
if (sceneIsLoaded) {
progressButtons = (
<>
<div {...{ className: "studio-intro-exporter" }}>
<div {...{ className: "exporter-title" }}>
<span>
Add camera motion to render
</span>
</div>
<div {...{ className: "exporter-controls" }}>
<SegmentButtons {...{ onChange: ({ target }: { target: any }) => {
cameraSet(target.value)
}, options: cameraOpts, value: camera }}/>
<Button label="Create Movie from 3D Scene" onClick={onClick} />
</div>
</div>
</>
)
}
return (
<div className="studio-intro-page">
<Scene3D
@@ -91,21 +131,11 @@ function StudioIntroPage(props: Props) {
mode={EngineMode.Studio}
asset={assetDescriptor}
className="flex-grow-1"
onSceneReadyCallback={onSceneReadyCallback}
onSceneSavedCallback={onSaveCallback}
/>
<div {...{ className: "studio-intro-exporter" }}>
<div {...{ className: "exporter-title" }}>
<span>
Add camera motion to render
</span>
</div>
<div {...{ className: "exporter-controls" }}>
<SegmentButtons {...{ onChange: ({ target }: { target: any }) => {
cameraSet(target.value)
}, options: cameraOpts, value: camera }}/>
<Button label="Create Movie from 3D Scene" onClick={onClick} />
</div>
</div>
{progressButtons}
</div>
);
}
@@ -75,23 +75,23 @@ export default function PageVSTApp() {
const styleOptions = [
{
label: "Anime",
imageUrl: "/images/dummy-image.jpg",
label: "2D Anime",
imageUrl: "/images/landing/onboarding/styles/style-2d-anime.webp",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Pixel",
imageUrl: "/images/dummy-image-2.jpg",
label: "3D Cartoon",
imageUrl: "/images/landing/onboarding/styles/style-3d-cartoon.webp",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Painting",
imageUrl: "/images/dummy-image-3.jpg",
label: "Ink B&W",
imageUrl: "/images/landing/onboarding/styles/style-ink-bw.webp",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Ink",
imageUrl: "/images/dummy-image-4.jpg",
label: "Origami",
imageUrl: "/images/landing/onboarding/styles/style-origami.webp",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
];