Merge pull request #293 from storytold/olivicmic-fy-faceani-edu-video

Face Animator Sample Video
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-10-06 18:30:49 -04:00
commit 7c5dfdd15c
15 files changed
+324 -200

No files matched your search

@@ -0,0 +1,48 @@
@import "scss/colours.scss";
.fy-basic-video {
overflow: hidden;
width: 100%;
margin: 0;
height: fit-content;
position: relative;
video, .video-overlay {
border-radius: .5rem;
width: 100%;
height: 100%;
}
video {object-fit: cover; }
.video-overlay {
background: linear-gradient(rgba($primary,0.5) 0%, rgba($secondary,0.75) 75%);
cursor: pointer;
padding: 2rem;
position: absolute;
top: 0px;
h6 {
color: #fff;
filter: drop-shadow(0 3px 3px rgba($primary-dark,.25));
font-size: 1.5rem;
text-align: center;
}
svg {
filter: drop-shadow(0 3px 7px rgba($primary-dark,.45));
position: absolute;
width: 3rem;
height: 3rem;
inset: 50%;
transform: translate(-50%, -50%);
}
.video-play-button {
background-color: green;
width: 4rem;
height: 4rem;
stroke: white;
}
}
};
@@ -0,0 +1,35 @@
import React, { useState } from "react";
import { a, useSpring } from "@react-spring/web";
import { useVideo } from "hooks";
import makeClass from "resources/makeClass";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlay } from "@fortawesome/free-solid-svg-icons";
import "./BasicVideo.scss"
interface Props {
className?: string;
src?: string;
}
export default function BasicVideo({ className, src }: Props) {
const [tint,tintSet] = useState(true);
const [{ playCtrl },vidProps] = useVideo({ onEnded: () => tintSet(true) });
const style = useSpring({
onRest: () => { if (!tint) { playCtrl!(tintSet) }},
opacity: tint ? 1 : 0
});
const onClick = () => {
tintSet(!tint)
if (!tint) playCtrl!();
};
return <div {...{ ...makeClass("fy-basic-video",className) }}>
<video {...{ playsInline: true, ...vidProps }}>
<source {...{ src, type: "video/mp4" }} />
</video>
<a.div {...{ className: "video-overlay", onClick, style }}>
<h6>Face Animator Sample</h6>
<FontAwesomeIcon {...{ icon: faPlay }} />
</a.div>
</div>;
};
@@ -0,0 +1 @@
export { default } from './BasicVideo';
@@ -4,9 +4,9 @@
.image-upload-frame {
// aspect-ratio: 4/3;
// @include breakpoint(min-width 992px) {
// aspect-ratio: 1/1;
// }
@include breakpoint(min-width 992px) {
aspect-ratio: 1/1;
}
}
.fy-image-uploader {
@@ -1,29 +1,31 @@
import AudioInput from './AudioInput';
import Check from './Check';
import Checkbox from './Checkbox';
import FileActions from './FileActions';
import FileDetails from './FileDetails';
import FileWrapper from './FileWrapper';
import FileLabel from './FileLabel';
import ImageInput from './ImageInput';
import NumberSlider from './NumberSlider';
import Radio from './Radio';
import SegmentButtons from './SegmentButtons';
import Slider from './Slider';
import Spinner from './Spinner';
import AudioInput from "./AudioInput";
import BasicVideo from "./BasicVideo";
import Check from "./Check";
import Checkbox from "./Checkbox";
import FileActions from "./FileActions";
import FileDetails from "./FileDetails";
import FileWrapper from "./FileWrapper";
import FileLabel from "./FileLabel";
import ImageInput from "./ImageInput";
import NumberSlider from "./NumberSlider";
import Radio from "./Radio";
import SegmentButtons from "./SegmentButtons";
import Slider from "./Slider";
import Spinner from "./Spinner";
export {
AudioInput,
Check,
Checkbox,
FileActions,
FileDetails,
FileWrapper,
FileLabel,
ImageInput,
NumberSlider,
Radio,
SegmentButtons,
Slider,
Spinner
AudioInput,
BasicVideo,
Check,
Checkbox,
FileActions,
FileDetails,
FileWrapper,
FileLabel,
ImageInput,
NumberSlider,
Radio,
SegmentButtons,
Slider,
Spinner
};
+2 -1
View File
@@ -3,5 +3,6 @@ import useHover from './useHover';
import useId from './useId';
import useIdempotency from './useIdempotency';
import useLocalize from './useLocalize';
import useVideo from './useVideo';
export { useFile, useHover, useId, useIdempotency, useLocalize };
export { useFile, useHover, useId, useIdempotency, useLocalize, useVideo };
@@ -0,0 +1 @@
export { default } from './useVideo';
@@ -0,0 +1,23 @@
import { useRef } from 'react';
interface Props {
onEnded?: any;
}
export default function useVideo({ onEnded }: Props) {
const ref = useRef<HTMLVideoElement>(null);
const playCtrl = (toDo = (b:any) => {}) => {
if (ref.current) {
let isPlaying = !!(ref.current.currentTime > 0 && !ref.current.paused && !ref.current.ended && ref.current.readyState > 2);
if (!isPlaying) {
toDo(false);
ref.current.play();
} else {
toDo(true);
ref.current.pause();
}
}
};
return [{ playCtrl },{ onEnded, ref }];
};
@@ -0,0 +1,63 @@
import React from "react";
import { animated } from "@react-spring/web";
import { AudioInput, BasicVideo, Checkbox, ImageInput, SegmentButtons } from "components/common";
import { FaceAnimatorSlide } from "./FaceAnimatorTypes";
export default function FaceAnimatorInput({
audioProps,
imageProps,
frameDimensions,
frameDimensionsChange,
disableFaceEnhancement,
disableFaceEnhancementChange,
still,
stillChange,
toggle,
style,
t,
removeWatermark,
removeWatermarkChange, }: FaceAnimatorSlide) {
return <animated.div {...{ className: "lipsync-editor row", style }}>
<div {...{ className: "media-input-column col-lg-6" }}>
<h5>{t("headings.image")}</h5>
<ImageInput
{...{
...imageProps,
onRest: () => toggle.image(imageProps.file ? true : false),
}}
/>
</div>
<div {...{ className: "media-input-column audio-input-column col-lg-6" }}>
<h5>{t("headings.audio")}</h5>
<AudioInput
{...{
...audioProps,
onRest: (p: any, c: any, item: any, l: any) => {
toggle.audio(!!audioProps.file);
},
hideActions: true,
}}
/>
<BasicVideo {...{ className: "face-animator-wide-sample", src: "/videos/face-animator-instruction-en.mp4" }}/>
</div>
<div {...{ className: "animation-configure-panel panel" }}>
<fieldset {...{ className: "input-block" }}>
<legend>Video Dimensions</legend>
<SegmentButtons {...{
onChange: frameDimensionsChange,
options: [{ label: "Landscape (Wide)", value: "twitter_landscape" },{ label: "Portrait (Tall)", value: "twitter_portrait" },{ label: "Square", value: "twitter_square" }],
value: frameDimensions
}}/>
</fieldset>
<fieldset {...{ className: "input-block" }}>
<legend>Watermark</legend>
<Checkbox {...{ checked: removeWatermark, label: "Remove Watermark (premium only)", onChange: removeWatermarkChange }}/>
</fieldset>
<fieldset {...{ className: "input-block" }}>
<legend>Animation</legend>
<Checkbox {...{ checked: still, label: "Reduce Movement (not recommended)", onChange: stillChange}}/>
<Checkbox {...{ checked: disableFaceEnhancement, label: "Disable Face Enhancer (not recommended)", onChange: disableFaceEnhancementChange}}/>
</fieldset>
</div>
</animated.div>;
};
@@ -11,20 +11,11 @@ import { JobState } from "@storyteller/components/src/jobs/JobStates";
import { Analytics } from "common/Analytics";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDownload } from "@fortawesome/free-solid-svg-icons";
interface Props {
audioProps: any;
imageProps: any;
style: any;
enqueueInferenceJob: any;
sessionSubscriptionsWrapper: any;
inferenceJobsByCategory: any;
t: any;
}
import { FaceAnimatorSlide } from "./FaceAnimatorTypes";
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
export default function FaceAnimatorSuccess({ audioProps, imageProps, style, enqueueInferenceJob, sessionSubscriptionsWrapper, inferenceJobsByCategory, t }: Props) {
export default function FaceAnimatorSuccess({ audioProps, imageProps, style, enqueueInferenceJob, sessionSubscriptionsWrapper, inferenceJobsByCategory, t }: FaceAnimatorSlide) {
const [pending, pendingSet] = useState<GetPendingTtsJobCountSuccessResponse>({
success: true,
pending_job_count: 0,
@@ -0,0 +1,33 @@
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
export interface FaceAnimatorSlide {
audioProps: any;
children?: any;
imageProps: any;
frameDimensions: any;
frameDimensionsChange: any;
disableFaceEnhancement: any;
disableFaceEnhancementChange: any;
index: number;
still: any;
stillChange: any;
style: any;
toggle: any;
enqueueInferenceJob: any;
sessionSubscriptionsWrapper: any;
t: any;
inferenceJobsByCategory: any;
removeWatermark: any;
removeWatermarkChange: any;
}
export interface FaceAnimatorCore {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}
@@ -0,0 +1,42 @@
import React from 'react';
import { a, useTransition } from "@react-spring/web";
import { Spinner } from "components/common";
import { springs } from "resources";
import { FaceAnimatorSlide } from "./FaceAnimatorTypes";
export default function FaceAnimatorWorking({ audioProps, imageProps, index, style, t }: FaceAnimatorSlide) {
const workStatus = [
"",
t("status.uploadingAudio"),
t("status.uploadingImage"),
t("status.requestingAnimation"),
"",
];
const transitions = useTransition(index, {
...springs.soft,
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
});
return <a.div {...{ className: "lipsync-working", style }}>
<div {...{ className: "lipsync-working-notice" }}>
<h2 {...{ className: "working-title" }}>
{" "}
{transitions(({ opacity, position }, i) => {
return (
<a.span
{...{
className: "working-title-text",
style: { opacity, position: position as any },
}}
>
{workStatus[index]} ...
</a.span>
);
})}
</h2>
<Spinner />
</div>
</a.div>;
};
@@ -1,5 +1,8 @@
@import "scss/_breakpoint.scss";
@import "scss/colours.scss";
$under992: max-width 992px;
$over992: min-width 992px;
.lipsync-editor {
padding-top: 2rem;
@@ -13,13 +16,18 @@ $under992: max-width 992px;
}
}
.sub-title {
display: block;
margin-top: 1rem;
.audio-input-column {
display: flex;
flex: 1;
flex-direction: column;
.fy-audio-uploder {
flex: 1 0 auto;
}
}
.animation-configure-panel {
margin: 1rem 0 -.5rem;
margin: 1.5rem 0 -.5rem;
padding: 1rem;
display: flex;
flex-wrap: wrap;
@@ -123,3 +131,22 @@ $under992: max-width 992px;
font-size: 0.75rem;
}
}
.face-animator-mobile-sample {
padding: 1rem;
@include breakpoint($over992) {
display: none;
}
}
.face-animator-wide-sample {
flex: 1;
height: auto;
@include breakpoint($under992) {
display: none;
}
video { position: absolute ;}
}
@@ -1,8 +1,7 @@
import React, { useState } from "react";
import { animated, useTransition } from "@react-spring/web";
import { useTransition } from "@react-spring/web";
import { v4 as uuidv4 } from "uuid";
import { useFile, useLocalize } from "hooks";
import { AudioInput, Checkbox, ImageInput, SegmentButtons, Spinner } from "components/common";
import { springs } from "resources";
import {
UploadAudio,
@@ -19,162 +18,21 @@ import {
// EnqueueFaceAnimationIsSuccess,
// EnqueueFaceAnimationRequest,
} from "@storyteller/components/src/api/face_animation/EnqueueFaceAnimation";
// import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import FaceAnimatorTitle from "./FaceAnimatorTitle";
import FaceAnimatorInput from './FaceAnimatorInput';
import FaceAnimatorWorking from './FaceAnimatorWorking';
import FaceAnimatorSuccess from "./FaceAnimatorSuccess";
import { FaceAnimatorCore } from "./FaceAnimatorTypes";
import { BasicVideo } from "components/common";
import "./LipsyncEditor.scss";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
interface LipSyncProps {
// animationChange: any;
// animationStyle: any;
audioProps: any;
children?: any;
// cropChange: any;
// cropping: any;
imageProps: any;
frameDimensions: any;
frameDimensionsChange: any;
disableFaceEnhancement: any;
disableFaceEnhancementChange: any;
index: number;
still: any;
stillChange: any;
style: any;
toggle: any;
enqueueInferenceJob: any;
sessionSubscriptionsWrapper: any;
t: any;
inferenceJobsByCategory: any;
removeWatermark: any;
removeWatermarkChange: any;
}
interface EditorProps {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
}
const softSpring = { config: { mass: 1, tension: 80, friction: 10 } };
const InputPage = ({
// animationChange,
// animationStyle,
audioProps,
// cropping,
// cropChange,
imageProps,
frameDimensions,
frameDimensionsChange,
disableFaceEnhancement,
disableFaceEnhancementChange,
still,
stillChange,
toggle,
style,
t,
removeWatermark,
removeWatermarkChange,
}: LipSyncProps) => {
return <animated.div {...{ className: "lipsync-editor row", style }}>
<div {...{ className: "media-input-column col-lg-6" }}>
<h5>{t("headings.image")}</h5>
<ImageInput
{...{
...imageProps,
onRest: () => toggle.image(imageProps.file ? true : false),
}}
/>
</div>
<div {...{ className: "media-input-column col-lg-6" }}>
<h5>{t("headings.audio")}</h5>
<AudioInput
{...{
...audioProps,
onRest: (p: any, c: any, item: any, l: any) => {
toggle.audio(!!audioProps.file);
},
hideActions: true,
}}
/>
</div>
<div {...{ className: "animation-configure-panel panel" }}>
<fieldset {...{ className: "input-block" }}>
<legend>Video Dimensions</legend>
<SegmentButtons {...{
onChange: frameDimensionsChange,
options: [{ label: "Landscape (Wide)", value: "twitter_landscape" },{ label: "Portrait (Tall)", value: "twitter_portrait" },{ label: "Square", value: "twitter_square" }],
value: frameDimensions
}}/>
</fieldset>
<fieldset {...{ className: "input-block" }}>
<legend>Watermark</legend>
<Checkbox {...{ checked: removeWatermark, label: "Remove Watermark (premium only)", onChange: removeWatermarkChange }}/>
</fieldset>
<fieldset {...{ className: "input-block" }}>
<legend>Animation</legend>
<Checkbox {...{ checked: still, label: "Reduce Movement (not recommended)", onChange: stillChange}}/>
<Checkbox {...{ checked: disableFaceEnhancement, label: "Disable Face Enhancer (not recommended)", onChange: disableFaceEnhancementChange}}/>
</fieldset>
</div>
</animated.div>;
};
const Working = ({ audioProps, imageProps, index, style, t }: LipSyncProps) => {
const workStatus = [
"",
t("status.uploadingAudio"),
t("status.uploadingImage"),
t("status.requestingAnimation"),
"",
];
const transitions = useTransition(index, {
...springs.soft,
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
});
return (
<animated.div {...{ className: "lipsync-working", style }}>
<div {...{ className: "lipsync-working-notice" }}>
<h2 {...{ className: "working-title" }}>
{" "}
{transitions(({ opacity, position }, i) => {
return (
<animated.span
{...{
className: "working-title-text",
style: { opacity, position: position as any },
}}
>
{workStatus[index]} ...
</animated.span>
);
})}
</h2>
<Spinner />
</div>
</animated.div>
);
};
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
export default function LipsyncEditor({
enqueueInferenceJob,
sessionSubscriptionsWrapper,
inferenceJobsByCategory,
...rest
}: EditorProps) {
}: FaceAnimatorCore) {
const { t } = useLocalize("FaceAnimator");
// the ready states are set by functions which run after the upload input animation is completed, which then illuminates the respective checkmark in a staggered way to draw attention to the workflow, and reduces concurrent animations
@@ -234,10 +92,6 @@ export default function LipsyncEditor({
if ("upload_token" in responses.image) {
indexSet(3); // set face animator API working page
return EnqueueFaceAnimation({
// backend_animation_key: animationStyle,
// backend_cropping_key: cropping === "cropped",
// backend_orientation_key: orientation === "landscape",
// backend_watermark_key: watermark,
uuid_idempotency_token: uuidv4(),
audio_source: {
maybe_media_upload_token: responses.audio.upload_token,
@@ -279,7 +133,7 @@ export default function LipsyncEditor({
};
const transitions = useTransition(index, {
...softSpring,
...springs.soft,
from: { opacity: 0, position: "absolute" },
enter: { opacity: 1, position: "relative" },
leave: { opacity: 0, position: "absolute" },
@@ -290,7 +144,7 @@ export default function LipsyncEditor({
<div {...{ className: "panel lipsync-panel p-3 py-4 p-md-4" }}>
<FaceAnimatorTitle {...headerProps} />
{transitions((style, i) => {
const Page = [InputPage, Working, FaceAnimatorSuccess][page];
const Page = [FaceAnimatorInput, FaceAnimatorWorking, FaceAnimatorSuccess][page];
return Page ? (
<Page
{...{
@@ -318,6 +172,9 @@ export default function LipsyncEditor({
);
})}
</div>
<div {...{ className: "face-animator-mobile-sample" }}>
<BasicVideo {...{ src: "/videos/face-animator-instruction-en.mp4" }}/>
</div>
</div>
);
}