diff --git a/src/website/packages/fakeyou.com/public/static-404.css b/src/website/packages/fakeyou.com/public/static-404.css
index c5f46377eb..c63958b0f9 100644
--- a/src/website/packages/fakeyou.com/public/static-404.css
+++ b/src/website/packages/fakeyou.com/public/static-404.css
@@ -2,7 +2,8 @@
--primary: #e66462;
--primary-light: #ea7a78;
}
-html, body {
+html,
+body {
background-color: #1a1a27;
color: #fff;
font-family: "Source Sans 3";
@@ -13,7 +14,7 @@ html, body {
h1 {
font-size: 2.5rem;
- margin-bottom: .5rem;
+ margin-bottom: 0.5rem;
line-height: 1em;
}
@@ -44,14 +45,14 @@ h1 {
display: flex;
flex-direction: column;
background-color: #242433;
- border: 2px solid rgba(255, 255, 255, .04);
- border-radius: .5rem;
+ border: 2px solid rgba(255, 255, 255, 0.04);
+ border-radius: 0.5rem;
box-sizing: border-box;
height: 330px;
padding: 2rem 1.5rem 0;
text-decoration: none;
color: #fff;
- transition: .2s all;
+ transition: 0.2s all;
overflow: hidden;
}
@@ -62,30 +63,32 @@ h1 {
.arrow-button {
display: inline-block;
background-color: var(--primary);
- border-radius: .5rem;
+ border-radius: 0.5rem;
height: 2.5rem;
width: 2.5rem;
- transition: .2s all;
+ transition: 0.2s all;
}
.tools-grid-item:hover .arrow-button {
background-color: var(--primary-light);
}
-.arrow-button line, .arrow-button polyline {
+.arrow-button line,
+.arrow-button polyline {
stroke: #fff;
stroke-width: 2.5px;
stroke-linecap: round;
fill: none;
- transition: .2s all;
+ transition: 0.2s all;
}
-.tools-grid-item:hover line, .tools-grid-item:hover polyline {
+.tools-grid-item:hover line,
+.tools-grid-item:hover polyline {
transform: translateX(3px);
}
.hanashi-banner {
- transition: .2s all ease-in-out;
+ transition: 0.2s all ease-in-out;
}
.tools-grid-item:hover .hanashi-banner {
@@ -95,11 +98,11 @@ h1 {
.banner-art {
width: 100%;
height: 100%;
- border-radius: .5rem .5rem 0 0;
+ border-radius: 0.5rem 0.5rem 0 0;
overflow: hidden;
}
.banner-art img {
width: 100%;
height: auto;
-}
\ No newline at end of file
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.scss b/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.scss
new file mode 100644
index 0000000000..0757814636
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.scss
@@ -0,0 +1,50 @@
+@import "scss/colours.scss";
+@import "scss/site-refresh.scss";
+
+.fy-camera-input {
+ // background: red;
+ border: lighten($panel, 6%) solid 2px;
+ // height: 100%;
+ width: 100%;
+ aspect-ratio: 1;
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ cursor: pointer;
+
+ .fy-camera-input-preview {
+ // background: red;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ margin-bottom: 0;
+ top: 0;
+ // margin: ;
+ padding: 0;
+
+ video {
+ height: 100%;
+ }
+
+ .fy-camera-input-float-btn {
+ background-color: rgba($primary, 0.85);
+ backdrop-filter: blur(5px);
+ position: absolute;
+ top: 0.5rem;
+ margin: auto;
+ left: 0.5rem;
+ right: 0.5rem;
+ }
+ }
+
+ .fy-camera-input-toggle-container {
+ @include video-controls;
+ border-radius: 0.5rem;
+ padding: 0.5rem 0.5rem 0.25rem;
+ left: 0;
+ right: 0;
+ margin: auto;
+ bottom: 0.5rem;
+ position: absolute;
+ }
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.tsx b/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.tsx
new file mode 100644
index 0000000000..3e3f0e8f6c
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/CameraInput/CameraInput.tsx
@@ -0,0 +1,130 @@
+import React, { useState } from "react";
+import Webcam from "react-webcam";
+import { a, TransitionFn, useTransition } from "@react-spring/web";
+import { useCameraState, useMedia, useModal } from "hooks";
+import { CameraCapture } from "components/modals";
+import { Button, RecordToggle } from "components/common";
+import "./CameraInput.scss";
+
+export interface CameraInputEvent {
+ target: {
+ value: string;
+ };
+}
+
+interface CameraInputProps {
+ GApage?: string;
+ onChange?: (e: CameraInputEvent) => void;
+ value?: any;
+}
+
+export default function CameraInput({
+ GApage = "/",
+ onChange,
+ value,
+}: CameraInputProps) {
+ const { open } = useModal();
+ const camera = useCameraState(true);
+ const [token, tokenSet] = useState("");
+ const { bucketUrl } = useMedia({ mediaToken: token });
+
+ const cameraClick = () =>
+ open({
+ component: CameraCapture,
+ padding: false,
+ props: {
+ autoCapture: true,
+ camera,
+ GApage,
+ selectToken: (token: string) => {
+ if (onChange) {
+ tokenSet(token);
+ onChange({ target: { value: token } });
+ }
+ },
+ },
+ width: "square",
+ });
+
+ const transitions: TransitionFn<
+ boolean,
+ { opacity: number; transform: string }
+ > = useTransition(!!token, {
+ config: { mass: 1, tension: 80, friction: 10 },
+ from: { opacity: 0, transform: `translateX(${5}rem)` },
+ enter: { opacity: 1, transform: `translateX(0)` },
+ leave: { opacity: 0, transform: `translateX(${5}rem)` },
+ });
+
+ const reset = () => {
+ cameraClick();
+ tokenSet("");
+ };
+
+ return (
+
+ {transitions((style: any, hasToken: boolean) =>
+ hasToken ? (
+
+
+
+
+ ) : (
+
+ cameraStartedSet(true),
+ ref: camera.ref,
+ videoConstraints: {
+ width: 512,
+ height: 512,
+ facingMode: "user",
+ // facingMode:
+ // cameraPosition === "user"
+ // ? cameraPosition
+ // : { exact: cameraPosition },
+ },
+ }}
+ />
+
+
+
+
+ )
+ )}
+
+ );
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/CameraInput/index.tsx b/src/website/packages/fakeyou.com/src/components/common/CameraInput/index.tsx
new file mode 100644
index 0000000000..4784bd520b
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/CameraInput/index.tsx
@@ -0,0 +1,2 @@
+export { default } from "./CameraInput";
+export type { CameraInputEvent } from "./CameraInput";
diff --git a/src/website/packages/fakeyou.com/src/components/common/InputDescription/InputDescription.tsx b/src/website/packages/fakeyou.com/src/components/common/InputDescription/InputDescription.tsx
new file mode 100644
index 0000000000..735c2d8062
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/InputDescription/InputDescription.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+interface Props {
+ className?: string;
+ description: string;
+ stepNumber: number;
+ title: string;
+}
+
+export default function InputDescription({
+ className,
+ description,
+ stepNumber,
+ title,
+}: Props) {
+ return (
+
+
+ {stepNumber &&
{stepNumber}
}
+
{title}
+
+
+
{description}
+
+ );
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/InputDescription/index.tsx b/src/website/packages/fakeyou.com/src/components/common/InputDescription/index.tsx
new file mode 100644
index 0000000000..6fd0949a5d
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/InputDescription/index.tsx
@@ -0,0 +1 @@
+export { default } from "./InputDescription";
diff --git a/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.scss b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.scss
new file mode 100644
index 0000000000..e7c5f17955
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.scss
@@ -0,0 +1,68 @@
+@import "scss/colours.scss";
+
+.fy-record-toggle {
+ appearance: none;
+ border: none;
+ background: none;
+ display: inline-flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ color: #fff;
+ font-size: 0.75rem;
+
+ * {
+ pointer-events: none;
+ }
+
+ svg {
+ display: block;
+ height: 2rem;
+ width: 2rem;
+ margin-bottom: 0.25rem;
+
+ &.fy-record-toggle-record {
+ circle {
+ stroke: $primary;
+ }
+ rect {
+ fill: $primary;
+ }
+ &:hover {
+ circle {
+ stroke: $primary-light;
+ }
+ rect {
+ fill: $primary-light;
+ }
+ }
+ }
+
+ &.fy-record-toggle-stop {
+ circle {
+ stroke: $secondary-lightest;
+ }
+ rect {
+ fill: $secondary-lightest;
+ }
+ &:hover {
+ circle {
+ stroke: #fff;
+ }
+ rect {
+ fill: #fff;
+ }
+ }
+ }
+
+ circle,
+ rect {
+ transition: all 0.25s ease-in-out;
+ }
+
+ circle {
+ fill: none;
+ stroke-width: 2px;
+ }
+ }
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.tsx b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.tsx
new file mode 100644
index 0000000000..dfabb7de63
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/RecordToggle.tsx
@@ -0,0 +1,87 @@
+import React from "react";
+import { a, useSpring } from "@react-spring/web";
+import "./RecordToggle.scss";
+
+export interface RecordToggleEvent {
+ target: {
+ value: boolean;
+ };
+}
+
+export interface RecordToggleProps {
+ className?: string;
+ counter?: number;
+ onChange?: (e: RecordToggleEvent) => void;
+ value: boolean;
+}
+
+export default function RecordToggle({
+ className,
+ counter = 0,
+ onChange,
+ value,
+}: RecordToggleProps) {
+ const hours = Math.floor(counter / 3600);
+ const minutes = Math.floor((counter - hours * 3600) / 60);
+ const seconds = counter - hours * 3600 - minutes * 60;
+
+ const timeString =
+ hours.toString().padStart(2, "0") +
+ ":" +
+ minutes.toString().padStart(2, "0") +
+ ":" +
+ seconds.toString().padStart(2, "0");
+
+ const mainToggleStyle = useSpring({
+ rx: value ? 1 : 8,
+ size: value ? 14 : 16,
+ xy: value ? 9 : 8,
+ });
+
+ const onClick = () => {
+ if (onChange) {
+ onChange({
+ target: {
+ value: !value,
+ },
+ });
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/website/packages/fakeyou.com/src/components/common/RecordToggle/index.tsx b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/index.tsx
new file mode 100644
index 0000000000..6cdafd3e91
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/components/common/RecordToggle/index.tsx
@@ -0,0 +1,2 @@
+export { default } from "./RecordToggle";
+export type { RecordToggleEvent, RecordToggleProps } from "./RecordToggle";
diff --git a/src/website/packages/fakeyou.com/src/components/common/index.tsx b/src/website/packages/fakeyou.com/src/components/common/index.tsx
index c8eaf2cd05..e0015c1de0 100644
--- a/src/website/packages/fakeyou.com/src/components/common/index.tsx
+++ b/src/website/packages/fakeyou.com/src/components/common/index.tsx
@@ -7,6 +7,7 @@ import BasicTabs from "./BasicTabs";
import BookmarkButton from "./BookmarkButton";
import Button from "./Button";
import ButtonRevertToDefault from "./ButtonRevertToDefault";
+import CameraInput, { CameraInputEvent } from "./CameraInput";
import Checkbox from "./Checkbox";
import { MediaCards, SkeletonCard, WeightsCards } from "./Card";
import Container from "./Container";
@@ -21,6 +22,7 @@ import FileLabel from "./FileLabel";
import ImageInput from "./ImageInput";
import Input from "./Input";
import InputSeed from "./InputSeed";
+import InputDescription from "./InputDescription";
import JobsClearButton from "./JobsClearButton";
import JobQueueTicker from "./JobQueueTicker";
import Label from "./Label";
@@ -35,6 +37,10 @@ import NumberSliderV2 from "./NumberSliderV2";
import Pagination from "./Pagination";
import Panel from "./Panel";
import Radio from "./Radio";
+import RecordToggle, {
+ RecordToggleEvent,
+ RecordToggleProps,
+} from "./RecordToggle";
import Scene3D from "./Scene3D/Scene3D";
import Searcher from "./Searcher";
import SegmentButtons from "./SegmentButtons";
@@ -72,6 +78,7 @@ export {
BasicTabs,
BookmarkButton,
Button,
+ CameraInput,
//Cards
DeviceNotSupported,
DropdownOptions,
@@ -90,6 +97,7 @@ export {
FileLabel,
ImageInput,
Input,
+ InputDescription,
InputSeed,
JobsClearButton,
JobQueueTicker,
@@ -106,6 +114,7 @@ export {
Pagination,
Panel,
Radio,
+ RecordToggle,
Scene3D,
SegmentButtons,
Searcher,
@@ -137,7 +146,10 @@ export {
export type {
ActionButtonProps,
+ CameraInputEvent,
ModalUtilities,
+ RecordToggleEvent,
+ RecordToggleProps,
SelectModalData,
Socials,
ZoomSliderOnChangeEvent,
diff --git a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx
index 9186d90353..5e69a2a053 100644
--- a/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx
+++ b/src/website/packages/fakeyou.com/src/components/entities/EntityInput/EntityInputEmpty.tsx
@@ -1,6 +1,7 @@
import React from "react";
import { SlideProps } from "./EntityInput";
import {
+ useCameraState,
// useMediaUploader,
useModal,
useSession,
@@ -53,17 +54,19 @@ export default function EntityInputEmpty({
}: EmptySlideProps) {
const { open } = useModal();
const { user } = useSession();
+ const camera = useCameraState();
+
const accepted = Array.isArray(accept) ? accept : [accept];
const inputMode = EntityInputMode[type];
const isMedia = inputMode === EntityInputMode.media;
const fileTypes = isMedia
? accepted
- .map((mediaCategory, i) => {
- return mediaCategory
- ? getMediaTypesByCategory(mediaCategoryfromString(mediaCategory))
- : [];
- })
- .flat()
+ .map((mediaCategory, i) => {
+ return mediaCategory
+ ? getMediaTypesByCategory(mediaCategoryfromString(mediaCategory))
+ : [];
+ })
+ .flat()
: [];
const props: MediaBrowserProps = {
@@ -84,8 +87,9 @@ export default function EntityInputEmpty({
component: CameraCapture,
padding: false,
props: {
+ camera,
GApage,
- selectToken
+ selectToken,
},
width: "square",
});
@@ -99,8 +103,9 @@ export default function EntityInputEmpty({
return faFile;
};
- const supported = `${fileTypes.length ? fileTypes.join(", ") : fileTypes[0]
- } files supported`;
+ const supported = `${
+ fileTypes.length ? fileTypes.join(", ") : fileTypes[0]
+ } files supported`;
return (
<>
diff --git a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx
index fad2363c01..e01af3dee7 100644
--- a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx
+++ b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx
@@ -79,7 +79,7 @@ export default function AITools() {
imgAlt: "Video Compositor",
},
{
- to: "/beta/webcam-acting",
+ to: "/webcam-acting",
title: "Webcam Acting",
text: "Act as your character through your camera",
videoSrc: "/videos/ai-tools/ca_video.mp4",
diff --git a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss
index d91152fce9..bcaf1bb072 100644
--- a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss
+++ b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.scss
@@ -28,6 +28,10 @@
position: absolute;
width: 100%;
height: 100%;
+
+ video {
+ height: 100%;
+ }
}
.fy-camera-capture-centered {
diff --git a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx
index 13f4afd2d0..098c2abac2 100644
--- a/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx
+++ b/src/website/packages/fakeyou.com/src/components/modals/CameraCapture/CameraCapture.tsx
@@ -1,12 +1,13 @@
-import React, { useCallback, useEffect, useRef, useState } from "react";
+import React, { useState } from "react";
import Webcam from "react-webcam";
-import { a, TransitionFn, useSpring, useTransition } from "@react-spring/web";
+import { a, TransitionFn, useTransition } from "@react-spring/web";
import { WorkIndicator } from "components/svg";
-import { useInterval, useMediaUploader } from "hooks";
+import { CameraState, useCameraState, useMediaUploader } from "hooks";
import { UploaderResponse } from "components/entities/EntityTypes";
import {
Button,
ModalUtilities,
+ RecordToggle,
Spinner,
SegmentButtons,
} from "components/common";
@@ -16,80 +17,21 @@ import CapturePreview from "./CapturePreview";
import "./CameraCapture.scss";
interface CameraCaptureProps extends ModalUtilities {
+ autoCapture?: boolean;
+ camera: CameraState;
GApage?: string;
selectToken: (token: string) => void;
}
export default function CameraCapture({
+ autoCapture,
handleClose,
GApage,
selectToken,
}: CameraCaptureProps) {
- const webcamRef = useRef(null);
- const mediaRecorderRef = useRef(null);
- const [capturing, capturingSet] = useState(false);
- const [recordedChunks, recordedChunksSet] = useState([]);
- const [counter, counterSet] = useState(0);
- const [blob, blobSet] = useState(null);
- const [cameraStarted, cameraStartedSet] = useState(false);
const [cameraPosition, cameraPositionSet] = useState("user");
- const hours = Math.floor(counter / 3600);
- const minutes = Math.floor((counter - hours * 3600) / 60);
- const seconds = counter - hours * 3600 - minutes * 60;
-
- const timeString =
- hours.toString().padStart(2, "0") +
- ":" +
- minutes.toString().padStart(2, "0") +
- ":" +
- seconds.toString().padStart(2, "0");
-
- const handleDataAvailable = useCallback(
- ({ data }) => {
- if (data.size > 0) {
- recordedChunksSet(prev => prev.concat(data));
- }
- },
- [recordedChunksSet]
- );
-
- const mainToggleClick = () => {
- if (!capturing) {
- capturingSet(true);
-
- if (webcamRef.current && webcamRef.current.stream) {
- mediaRecorderRef.current = new MediaRecorder(webcamRef.current.stream, {
- mimeType: "video/mp4",
- });
-
- mediaRecorderRef.current.addEventListener(
- "dataavailable",
- handleDataAvailable
- );
- mediaRecorderRef.current.start();
- }
- } else {
- capturingSet(false);
-
- if (mediaRecorderRef.current !== null) {
- mediaRecorderRef.current.stop();
- }
- }
- };
-
- const mainToggleStyle = useSpring({
- rx: capturing ? 1 : 8,
- size: capturing ? 14 : 16,
- xy: capturing ? 9 : 8,
- });
-
- const resetCapture = () => {
- cameraStartedSet(false);
- blobSet(null);
- recordedChunksSet([]);
- counterSet(0);
- };
+ const camera = useCameraState(autoCapture);
const {
busy: uploaderBusy,
@@ -100,9 +42,9 @@ export default function CameraCapture({
onError: () => {
// @ts-ignore
window.dataLayer.push({
- "event": "upload_failure_webcam",
- "page": GApage || "/",
- "user_id": "$user_id"
+ event: "upload_failure_webcam",
+ page: GApage || "/",
+ user_id: "$user_id",
});
},
onSuccess: (res: UploaderResponse) => {
@@ -115,13 +57,15 @@ export default function CameraCapture({
? 3
: uploaderBusy
? 2
- : blob && !capturing
+ : camera.blob && !camera.capturing
? 1
: 0;
const upload = () => {
- if (blob) {
- const file = new File([blob], "test-upload.mp4", { type: "video/mp4" });
+ if (camera.blob) {
+ const file = new File([camera.blob], "test-upload.mp4", {
+ type: "video/mp4",
+ });
createUpload(file);
}
};
@@ -141,27 +85,6 @@ export default function CameraCapture({
{ label: "Rear camera", value: "enviroment" },
];
- useInterval({
- eventProps: { capturing, counter },
- interval: 1000,
- locked: !capturing,
- onTick: () => {
- if (capturing) {
- counterSet(currentCounter => currentCounter + 1);
- }
- },
- });
-
- useEffect(() => {
- if (!blob && !capturing && recordedChunks.length) {
- blobSet(
- new Blob(recordedChunks, {
- type: "video/mp4",
- })
- );
- }
- }, [blob, capturing, recordedChunks]);
-
return (
cameraStartedSet(true),
- ref: webcamRef,
+ onUserMedia: () => camera.startedSet(true),
+ ref: camera.ref,
videoConstraints: {
width: 512,
height: 512,
@@ -196,40 +119,16 @@ export default function CameraCapture({
},
}}
/>
- {cameraStarted ? (
+ {camera.started ? (
-
+ />
{isMobile && (
;
+ reset: () => void;
+ started: boolean;
+ startedSet: (startedState: boolean) => void;
+ toggle: (e: RecordToggleEvent) => void;
+}
+
+export default function useCameraState(autoCapture = false): CameraState {
+ const ref = useRef(null);
+ const mediaRecorderRef = useRef(null);
+
+ const [blob, blobSet] = useState(null);
+ const [started, startedSet] = useState(false);
+ const [counter, counterSet] = useState(0);
+ const [capturing, capturingSet] = useState(false);
+ const [recordedChunks, recordedChunksSet] = useState([]);
+ const [autoTriggered, autoTriggeredSet] = useState(false);
+
+ // useEffect will only update when watching the stream value directly
+
+ const stream = ref?.current?.stream;
+
+ const handleDataAvailable = useCallback(
+ ({ data }) => {
+ if (data.size > 0) {
+ recordedChunksSet(prev => prev.concat(data));
+ }
+ },
+ [recordedChunksSet]
+ );
+
+ const startCapture = useCallback(
+ current => {
+ capturingSet(true);
+
+ if (current && current.stream) {
+ mediaRecorderRef.current = new MediaRecorder(current.stream, {
+ mimeType: "video/mp4",
+ });
+
+ mediaRecorderRef.current.addEventListener(
+ "dataavailable",
+ handleDataAvailable
+ );
+ mediaRecorderRef.current.start();
+ }
+ },
+ [handleDataAvailable]
+ );
+
+ const toggle = ({ target }: RecordToggleEvent) => {
+ if (target.value && ref.current) {
+ startCapture(ref.current);
+ } else {
+ capturingSet(false);
+
+ if (mediaRecorderRef.current !== null) {
+ mediaRecorderRef.current.stop();
+ }
+ }
+ };
+
+ const reset = () => {
+ startedSet(false);
+ blobSet(null);
+ recordedChunksSet([]);
+ counterSet(0);
+ };
+
+ useInterval({
+ eventProps: { capturing, counter },
+ interval: 1000,
+ locked: !capturing,
+ onTick: () => {
+ if (capturing) {
+ counterSet(currentCounter => currentCounter + 1);
+ }
+ },
+ });
+
+ useEffect(() => {
+ if (!blob && !capturing && recordedChunks.length) {
+ blobSet(
+ new Blob(recordedChunks, {
+ type: "video/mp4",
+ })
+ );
+ }
+
+ if (autoCapture && !capturing && !autoTriggered && ref.current && stream) {
+ autoTriggeredSet(true);
+ startCapture(ref.current);
+ }
+ }, [
+ autoCapture,
+ autoTriggered,
+ blob,
+ capturing,
+ recordedChunks,
+ startCapture,
+ stream,
+ ]);
+
+ return { blob, capturing, counter, ref, reset, started, startedSet, toggle };
+}
diff --git a/src/website/packages/fakeyou.com/src/hooks/useMedia/useMedia.tsx b/src/website/packages/fakeyou.com/src/hooks/useMedia/useMedia.tsx
index d31bba251d..e327601dd8 100644
--- a/src/website/packages/fakeyou.com/src/hooks/useMedia/useMedia.tsx
+++ b/src/website/packages/fakeyou.com/src/hooks/useMedia/useMedia.tsx
@@ -9,6 +9,7 @@ import {
} from "@storyteller/components/src/api/prompts/GetPrompts";
import { DeleteMedia } from "@storyteller/components/src/api/media_files/DeleteMedia";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
+import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
export default function useMedia({
debug = "",
@@ -41,6 +42,12 @@ export default function useMedia({
status === FetchStatus.in_progress ||
writeStatus === FetchStatus.in_progress;
+ // const bucketConfig = new BucketConfig();
+
+ const bucketUrl = media?.public_bucket_path
+ ? new BucketConfig().getGcsUrl(media.public_bucket_path)
+ : "";
+
useEffect(() => {
// this condidition handles all media file fetches
// it is triggered when status is ready and there is a media token but no media
@@ -80,6 +87,7 @@ export default function useMedia({
return {
busy,
+ bucketUrl,
media,
mediaFile: media,
mediaSet,
diff --git a/src/website/packages/fakeyou.com/src/scss/site-refresh.scss b/src/website/packages/fakeyou.com/src/scss/site-refresh.scss
index 255000f354..26e9091db7 100644
--- a/src/website/packages/fakeyou.com/src/scss/site-refresh.scss
+++ b/src/website/packages/fakeyou.com/src/scss/site-refresh.scss
@@ -63,6 +63,22 @@ styles with important tags:
backdrop-filter: blur(5px);
}
+@mixin video-controls {
+ @include video-tint;
+ position: absolute;
+ bottom: 1rem;
+ border-radius: 0.5rem;
+ padding: 0.5rem 0.5rem 0.25rem;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: 50%;
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ align-items: center;
+}
+
.fakeyou-refresh {
//- BS5 classnames -//
.object-fit-cover {
diff --git a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx
index 125c1234f5..83a9913daa 100644
--- a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx
+++ b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx
@@ -132,6 +132,7 @@ import SignUpSuccessPage from "./pages/beta_key/SignUpSuccessPage";
import { StudioMobileCheckPage } from "./pages/landing/storyteller/PostlaunchLanding/StudioMobileCheckPage";
import { UploadNewTtsModelPage } from "./pages/upload/UploadNewTtsModelPage";
import LivePortrait from "./pages/live_portrait/LivePortrait";
+import CameraLivePortrait from "./pages/live_portrait/CameraLivePortrait";
import { BetaLipSyncForm } from "./pages/beta_products/BetaLipSyncForm";
import { BetaLipSyncPage } from "./pages/beta_products/BetaLipSyncPage";
import { ToolsTestPage } from "./pages/tools_test/ToolsTestPage";
@@ -762,6 +763,10 @@ class PageContainer extends React.Component<
/>
+
+
+
+
{/* Route for initial voice creation */}
("public");
+ const hasPremium = sessionSubscriptions?.hasPaidFeatures();
+ const [generatedVideoSrc, setGeneratedVideoSrc] = useState("");
+ const [sourceTokens, setSourceTokens] = useState([
+ ...PRECOMPUTED_SOURCE_TOKENS,
+ ]);
+ const [motionTokens, setMotionTokens] = useState([
+ ...PRECOMPUTED_DRIVER_TOKENS,
+ ]);
+ const numberOfInitialSourceTokensRef = useRef(sourceTokens.length);
+ const numberOfInitialSourceTokens = numberOfInitialSourceTokensRef.current;
+ const numberOfInitialMotionTokensRef = useRef(motionTokens.length);
+ const numberOfInitialMotionTokens = numberOfInitialMotionTokensRef.current;
+ const [userSourceToken, setUserSourceToken] = useState("");
+ const [userMotionToken, setUserMotionToken] = useState("");
+ const [isUserUploaded, setIsUserUploaded] = useState(false);
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [generatedVideos, setGeneratedVideos] = useState([]);
+ const [jobProcessedTokens, setJobProcessedTokens] = useState([]);
+ const [currentlyGeneratingList, setCurrentlyGeneratingList] = useState<
+ CurrentlyGenerating[]
+ >([]);
+ const [jobProgress, setJobProgress] = useState({});
+ const [currentCombinationKey, setCurrentCombinationKey] = useState("");
+ const getCombinationKey = (sourceIndex: number, motionIndex: number) =>
+ `s${sourceIndex}_m${motionIndex}`;
+ const [cameraInputToken, cameraInputTokenSet] = useState("");
+
+ const location = useLocation();
+ const history = useHistory();
+
+ const handleJobClick = (job: InferenceJob) => {
+ const livePortraitDetails = job.maybeLivePortraitDetails;
+ if (livePortraitDetails) {
+ const { source_media_file_token, face_driver_media_file_token } =
+ livePortraitDetails;
+
+ const sourceIndex = sourceTokens.indexOf(source_media_file_token);
+ const motionIndex = motionTokens.indexOf(face_driver_media_file_token);
+ setSelectedSourceIndex(sourceIndex);
+ setSelectedMotionIndex(motionIndex);
+ setCurrentCombinationKey(getCombinationKey(sourceIndex, motionIndex));
+
+ // Find the video for the clicked job
+ const videoFromJob = generatedVideos.find(
+ video => video.jobToken === job.jobToken
+ );
+ if (videoFromJob) {
+ setGeneratedVideoSrc(videoFromJob.videoSrc);
+ setIsGenerating(false);
+ } else {
+ // If there's no video and the job is still in the generating list, show progress
+ const isGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === sourceIndex && gen.motionIndex === motionIndex
+ );
+ if (isGenerating) {
+ const progress =
+ jobProgress[getCombinationKey(sourceIndex, motionIndex)] || 0;
+ setJobProgress({
+ ...jobProgress,
+ [getCombinationKey(sourceIndex, motionIndex)]: progress,
+ });
+ setIsGenerating(true);
+ } else {
+ setIsGenerating(false);
+ setGeneratedVideoSrc("");
+ }
+ }
+ }
+ };
+
+ const handleJobStateChange = useCallback(
+ (jobToken: string, jobState: JobState) => {
+ const currentCombinationKey = getCombinationKey(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+
+ if (jobState === JobState.COMPLETE_FAILURE) {
+ setCurrentlyGeneratingList(prevList =>
+ prevList.filter(
+ gen =>
+ !(
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ )
+ )
+ );
+
+ setJobProgress(prevProgress => {
+ const updatedProgress = { ...prevProgress };
+ delete updatedProgress[currentCombinationKey];
+ return updatedProgress;
+ });
+
+ setIsGenerating(false);
+ }
+ },
+ [selectedSourceIndex, selectedMotionIndex]
+ );
+
+ const handleJobProgress = (progress: number | null) => {
+ setJobProgress(prevProgress => ({
+ ...prevProgress,
+ [currentCombinationKey]: progress,
+ }));
+ };
+
+ const handleSourceSelect = (index: number) => {
+ setIsUserUploaded(index >= numberOfInitialSourceTokens);
+ setSelectedSourceIndex(index);
+ setCurrentCombinationKey(getCombinationKey(index, selectedMotionIndex));
+ };
+
+ const enqueueClick = () => {
+ if (
+ !loggedInOrModal({
+ loginMessage: "Login to finish animating your portrait",
+ signupMessage: "Sign up to finish animating your portrait",
+ })
+ ) {
+ return;
+ }
+ // Clear the generated video when reanimating
+ setGeneratedVideoSrc("");
+ setIsGenerating(true);
+
+ // Add the current source and motion combination to the generating list
+ setCurrentlyGeneratingList(prevList => [
+ ...prevList,
+ { sourceIndex: selectedSourceIndex, motionIndex: selectedMotionIndex },
+ ]);
+
+ setIsEnqueuing(true);
+
+ const combinationKey = getCombinationKey(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+ setCurrentCombinationKey(combinationKey);
+
+ EnqueueFaceMirror("", {
+ creator_set_visibility: visibility,
+ face_driver_media_file_token: cameraInputToken,
+ maybe_crop: {
+ height: 0,
+ width: 0,
+ x: 0,
+ y: 0,
+ },
+ remove_watermark: removeWatermark,
+ source_media_file_token: sourceTokens[selectedSourceIndex],
+ uuid_idempotency_token: uuidv4(),
+ }).then((res: EnqueueFaceMirrorResponse) => {
+ if (res.success && res.inference_job_token) {
+ enqueueInferenceJob(
+ res.inference_job_token,
+ FrontendInferenceJobType.LivePortrait,
+ false
+ );
+ } else {
+ // @ts-ignore
+ window.dataLayer.push({
+ event: "enqueue_failure",
+ page: "/webcam-acting",
+ user_id: "$user_id",
+ });
+ console.error("Failed to enqueue job", res);
+ setIsGenerating(false);
+ // Remove the combination from currentlyGeneratingList if fail
+ setCurrentlyGeneratingList(prevList =>
+ prevList.filter(
+ gen =>
+ gen.sourceIndex !== selectedSourceIndex ||
+ gen.motionIndex !== selectedMotionIndex
+ )
+ );
+ }
+ setIsEnqueuing(false);
+ });
+ };
+
+ useEffect(() => {
+ if (userSourceToken) {
+ setSourceTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(userSourceToken);
+ if (tokenIndex !== -1) {
+ setSelectedSourceIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialSourceTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, userSourceToken];
+ setSelectedSourceIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ setUserSourceToken("");
+ }
+ }, [userSourceToken, numberOfInitialSourceTokens]);
+
+ useEffect(() => {
+ if (userMotionToken) {
+ setMotionTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(userMotionToken);
+ if (tokenIndex !== -1) {
+ setSelectedMotionIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialMotionTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, userMotionToken];
+ setSelectedMotionIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ setUserMotionToken("");
+ }
+ }, [userMotionToken, numberOfInitialMotionTokens]);
+
+ const handleOpenUploadSourceModal = () => {
+ open({
+ component: SourceEntityInput,
+ props: {
+ onChange: ({ target }: { target: any }) => {
+ setUserSourceToken(target.value);
+ close();
+ },
+ },
+ width: "small",
+ });
+ };
+
+ const [, setSelectedSourceMedia] = useState<{
+ [key: string]: any;
+ }>({});
+
+ const handleSelectedMediaChange = (media: any) => {
+ setSelectedSourceMedia(media);
+ };
+
+ const handleJobTokens = async (
+ maybeResultToken: string,
+ jobToken: string,
+ createdAt: Date,
+ livePortraitDetails?: LivePortraitDetails
+ ) => {
+ if (!livePortraitDetails) {
+ return;
+ }
+
+ // makes sure that it only processes each job once and exactly when needed
+ if (jobProcessedTokens.includes(jobToken)) {
+ return;
+ }
+
+ const response = await GetMedia(maybeResultToken, {});
+
+ if (
+ response &&
+ response.media_file &&
+ response.media_file.public_bucket_path
+ ) {
+ const mediaLink = new BucketConfig().getGcsUrl(
+ response.media_file.public_bucket_path
+ );
+
+ const sourceIndex = sourceTokens.indexOf(
+ livePortraitDetails.source_media_file_token
+ );
+ const motionIndex = motionTokens.indexOf(
+ livePortraitDetails.face_driver_media_file_token
+ );
+
+ const newGeneratedVideo = {
+ sourceIndex,
+ motionIndex,
+ sourceToken: livePortraitDetails.source_media_file_token,
+ motionToken: livePortraitDetails.face_driver_media_file_token,
+ videoSrc: mediaLink,
+ jobToken,
+ createdAt,
+ };
+
+ setGeneratedVideos(prevGeneratedVideos => {
+ return [
+ ...prevGeneratedVideos.filter(v => v.jobToken !== jobToken),
+ newGeneratedVideo,
+ ];
+ });
+
+ // Set the video source from the clicked job, regardless of its timestamp relative to others
+ if (
+ selectedSourceIndex === newGeneratedVideo.sourceIndex &&
+ selectedMotionIndex === newGeneratedVideo.motionIndex
+ ) {
+ setGeneratedVideoSrc(mediaLink);
+ setIsGenerating(false);
+ }
+
+ setJobProcessedTokens(prevTokens => [...prevTokens, jobToken]);
+ } else {
+ console.error(
+ "Failed to retrieve media or media has no public bucket path",
+ response
+ );
+ setIsGenerating(false);
+ setGeneratedVideoSrc("");
+ setIsEnqueuing(false);
+ }
+ };
+
+ const getLatestVideoForCombination = useCallback(
+ (sourceIndex: number, motionIndex: number) => {
+ const matchingVideos = generatedVideos.filter(
+ video =>
+ video.sourceIndex === sourceIndex && video.motionIndex === motionIndex
+ );
+ const sortedVideos = matchingVideos.sort(
+ (a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+ const latestVideo =
+ sortedVideos.length > 0 ? sortedVideos[0].videoSrc : null;
+
+ return latestVideo;
+ },
+ [generatedVideos]
+ );
+
+ useEffect(() => {
+ const latestVideoSrc = getLatestVideoForCombination(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+
+ if (latestVideoSrc) {
+ setGeneratedVideoSrc(latestVideoSrc);
+ setIsGenerating(false);
+ } else if (!isUserUploaded) {
+ setGeneratedVideoSrc("");
+ setIsGenerating(false);
+ } else {
+ setGeneratedVideoSrc("");
+ }
+ }, [
+ selectedSourceIndex,
+ selectedMotionIndex,
+ generatedVideos,
+ isUserUploaded,
+ getLatestVideoForCombination,
+ ]);
+
+ useEffect(() => {
+ const queryParams = new URLSearchParams(location.search);
+ const sourceToken = queryParams.get("source");
+ const motionToken = queryParams.get("motion");
+
+ if (sourceToken) {
+ setSourceTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(sourceToken);
+ if (tokenIndex !== -1) {
+ setSelectedSourceIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialSourceTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, sourceToken];
+ setSelectedSourceIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ }
+
+ if (motionToken) {
+ setMotionTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(motionToken);
+ if (tokenIndex !== -1) {
+ setSelectedMotionIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialMotionTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, motionToken];
+ setSelectedMotionIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ }
+ }, [
+ location.search,
+ numberOfInitialSourceTokens,
+ numberOfInitialMotionTokens,
+ ]);
+
+ const uploadFocusPointSource = useMemo(() => {
+ const queryParams = new URLSearchParams(location.search);
+ const sourceToken = queryParams.get("source");
+ const motionToken = queryParams.get("motion");
+
+ if (motionToken && !sourceToken) {
+ return true;
+ } else if (!motionToken && sourceToken) {
+ return false;
+ } else {
+ return false;
+ }
+ }, [location.search]);
+
+ useEffect(() => {
+ // When switching source or motion indexes, check if the current combination is generating
+ const isCurrentlyGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ );
+
+ if (isCurrentlyGenerating) {
+ setIsGenerating(true);
+ } else if (generatedVideoSrc) {
+ setIsGenerating(false);
+ } else if (!isUserUploaded) {
+ setIsGenerating(false);
+ } else {
+ setIsGenerating(false);
+ }
+ }, [
+ selectedSourceIndex,
+ selectedMotionIndex,
+ currentlyGeneratingList,
+ generatedVideoSrc,
+ isUserUploaded,
+ ]);
+
+ useEffect(() => {
+ if (generatedVideos.length > 0) {
+ const relevantVideos = generatedVideos.filter(video =>
+ currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === video.sourceIndex &&
+ gen.motionIndex === video.motionIndex
+ )
+ );
+
+ relevantVideos.forEach(video => {
+ if (video.jobToken) {
+ const jobIndex = currentlyGeneratingList.findIndex(
+ gen =>
+ gen.sourceIndex === video.sourceIndex &&
+ gen.motionIndex === video.motionIndex
+ );
+ if (jobIndex !== -1) {
+ const updatedGeneratingList = [...currentlyGeneratingList];
+ updatedGeneratingList.splice(jobIndex, 1);
+ setCurrentlyGeneratingList(updatedGeneratingList);
+
+ const isCurrentJobGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ );
+
+ if (!isCurrentJobGenerating) {
+ setGeneratedVideoSrc(video.videoSrc);
+ setIsGenerating(false);
+ }
+ }
+ }
+ });
+ }
+ }, [
+ generatedVideos,
+ currentlyGeneratingList,
+ selectedSourceIndex,
+ selectedMotionIndex,
+ ]);
+
+ const handleDownloadClick = () => {
+ if (generatedVideoSrc) {
+ const link = document.createElement("a");
+ link.href = generatedVideoSrc;
+ link.download = "output_video.mp4";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else {
+ console.error("No video source available for download");
+ }
+ };
+
+ const signupCTA = (
+ <>
+ {!sessionFetched ? null : (
+
+
+
+ You need to be logged in to use Live Portrait
+
+
+ Please login or sign up to continue.
+
+
+
+
+ )}
+ >
+ );
+
+ return (
+ <>
+
+
+
+
+ Webcam Acting
+
+
+ Use AI to transfer facial expressions, audio, and vocals from one
+ face video to an image or video.
+
+
+ {!loggedIn && (
+ {signupCTA}
+ )}
+
+
+
+
+
+
+
+
+
+ cameraInputTokenSet(target.value),
+ }}
+ />
+
+
+
+
+
+ history.push(
+ "/signup?redirect=/ai-live-portrait"
+ ),
+ isLoading: isEnqueuing || isGenerating,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ {
+ setVisibility(prevVisibility =>
+ prevVisibility === "private" ? "public" : "private"
+ );
+ }}
+ checked={visibility === "private"}
+ />
+
+ {
+ setRemoveWatermark(
+ prevRemoveWatermark => !prevRemoveWatermark
+ );
+ }}
+ checked={removeWatermark}
+ />
+
+
+ {!hasPremium && (
+
+
+
+ )}
+
+
+
+
+
+ {loggedIn && (
+
+
+
+
+ setSourceTokens(prevTokens =>
+ prevTokens.includes(newToken)
+ ? prevTokens
+ : [...prevTokens, newToken]
+ )
+ }
+ addMotionToken={(newToken: string) =>
+ setMotionTokens(prevTokens =>
+ prevTokens.includes(newToken)
+ ? prevTokens
+ : [...prevTokens, newToken]
+ )
+ }
+ onJobClick={handleJobClick}
+ onJobProgress={handleJobProgress}
+ onJobStateChange={handleJobStateChange}
+ />
+
+
+ )}
+
+
+
+
+
+
+ Try other AI video tools
+
+
+
+ >
+ );
+}