Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp

This commit is contained in:
Victor committed 2024-03-08 20:29:26 -08:00
commit e9ed1532e1
68 files changed
+1923 -291

No files matched your search

-1
View File
@@ -34,4 +34,3 @@ storyteller-frontend
Documentation [lives here](./_develop).
+3
View File
@@ -39,6 +39,9 @@
},
"dependencies": {
"@react-spring/types": "^9.7.3",
"@typeform/embed": "^4.8.1",
"@typeform/embed-react": "^3.15.0",
"local-storage": "^2.0.0",
"moment": "^2.29.4",
"universal-cookie": "^6.1.1",
"yarn": "^1.22.19"
@@ -20,6 +20,7 @@ export interface UserInfo {
// Rollout feature flags
can_access_studio: boolean,
maybe_feature_flags: string[],
// Usage
can_use_tts: boolean,
@@ -56,10 +56,17 @@ export class SessionWrapper {
public canAccessStudio(): boolean {
const hostnameAllowed = StudioRolloutHostnameAllowed();
const userAllowed =
this.sessionStateResponse?.user?.can_access_studio || false;
this.sessionStateResponse?.user?.can_access_studio ||
this.sessionStateResponse?.user?.maybe_feature_flags.includes("studio") ||
false;
return hostnameAllowed && userAllowed;
}
// Feature flag rollout for Video Style Transfer
public canAccessVideoStyleTransfer(): boolean {
return this.sessionStateResponse?.user?.maybe_feature_flags.includes("video_style_transfer") || false;
}
// Username is all lowercase
public getUsername(): string | undefined {
return this.sessionStateResponse?.user?.username;
+1
View File
@@ -75,6 +75,7 @@
"react-select": "^5.7.0",
"react-share": "^4.4.1",
"react-twitch-embed": "^3.0.1",
"react-typeform-embed": "^1.0.8",
"react-wavesurfer.js": "^0.0.8",
"sass": "^1.57.1",
"swiper": "^11.0.5",
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.
@@ -32,9 +32,9 @@ export default function DropdownMenu({
return (
<a.div style={fadeIn} className={`fy-dropdown-menu ${className}`.trim()}>
{items.map(item => (
<>
{item.onClick ? (
{items.map(item => {
if(item.onClick){
return(
<div
key={item.id}
onClick={e => {
@@ -53,27 +53,26 @@ export default function DropdownMenu({
{item.name}
</span>
</div>
) : (
<>
{item.link && (
<Link
key={item.id}
to={item.link}
className="fy-dropdown-item"
onClick={() => onClose()}
>
<span>
{item.icon && (
<FontAwesomeIcon icon={item.icon} className="me-2 fs-7" />
)}
{item.name}
</span>
</Link>
);
} else if (item.link) {
return(
<Link
key={item.id}
to={item.link}
className="fy-dropdown-item"
onClick={() => onClose()}
>
<span>
{item.icon && (
<FontAwesomeIcon icon={item.icon} className="me-2 fs-7" />
)}
</>
)}
</>
))}
{item.name}
</span>
</Link>
);
}//end if
return null;
})} {/*end items map */}
</a.div>
);
}
@@ -1,5 +1,5 @@
import React from "react";
import IframeResizer from "iframe-resizer-react";
import React, { useCallback, useEffect, useRef } from "react";
//import IframeResizer from "iframe-resizer-react";
import "./Scene3D.scss";
// See the following page for Storyteller Engine's query param documentation:
@@ -23,7 +23,70 @@ export default function Scene3D({
urlParams,
className,
}: Scene3DProps) {
let engineUrl = `https://engine.fakeyou.com?mode=${mode}`;
const engineBaseUrl = "https://engine.fakeyou.com";
const iframeRef = useRef<HTMLIFrameElement>(null);
const onMessage = useCallback((event: MessageEvent) => {
console.log("engine message received", event.data, event);
if (event.origin !== engineBaseUrl)
return;
if (event.data === "studio-ready") {
console.log("studio-ready message (1)");
const studio = iframeRef.current?.contentWindow;
if (!studio) return;
console.log("studio-ready message (2)");
studio.postMessage("save-scene", engineBaseUrl);
} else if (
typeof event.data === "string"
&& event.data.startsWith("scene-saved:")
) {
const mediaToken = event.data.match(/scene-saved:(.+)/)?.[1];
console.log("saved scene media token:", mediaToken);
} else if (event.data === "scene-save-failed") {
console.error("Failed to save the scene!");
}
}, []);
useEffect(() => {
/*
if (iframeRef && iframeRef.current) {
console.log("installing event listener for messages");
// NB: Compiler complains about ref going out of scope before destructor is called.
let ref = iframeRef.current;
// TODO(bt,2024-03-08): Fix typescript 'any' hack
(ref as any).addEventListener("message", onMessage);
return () => {
console.log("uninstalling event listener for messages");
// TODO(bt,2024-03-08): Fix typescript 'any' hack
(ref as any).removeEventListener("message", onMessage);
}
}
*/
console.log("installing event listener for messages");
window.addEventListener("message", onMessage);
return () => {
console.log("uninstalling event listener for messages");
window.removeEventListener("message", onMessage);
}
}, [onMessage]);
//}, [onMessage, iframeRef]);
let engineUrl = `${engineBaseUrl}/?mode=${mode}`;
if (sceneMediaFileToken) {
const sceneUrlRef = `remote://${sceneMediaFileToken}.scn.ron`;
@@ -32,13 +95,22 @@ export default function Scene3D({
engineUrl += `&objectId=${objectId}`;
}
console.log('installing iframe engine and callbacks');
return (
<div
className={`${
fullScreen ? "fy-scene-3d-fullscreen" : "fy-scene-3d-default"
} ${className ? className : ""}`.trim()}
>
<IframeResizer src={engineUrl} width="100%" height="100%" id="" />
{/*<IframeResizer src={engineUrl} width="100%" height="100%" id="" />*/}
<iframe
title="Storyteller Engine"
ref={iframeRef}
src={engineUrl}
width="100%"
height="100%"
/>
</div>
);
}
@@ -16,12 +16,17 @@
padding-top: 7.25px !important;
padding-bottom: 7.25px !important;
border-radius: 0.5rem 0 0 0.5rem !important;
background-color: $input-dark !important;
position: relative;
z-index: 3;
&::placeholder {
color: rgba($white, 0.3) !important;
}
&.landing-search {
background-color: rgba($input-dark, 0.5) !important;
}
}
}
@@ -32,11 +32,18 @@ export default memo(function SelectModalWrapper({
inputLabel: string;
onSelect: (data:SelectModalData) => void;
}) {
const [onSelectTimeStamp, setOnSelectTimeStamp] = useState<Date>(new Date());
const tabs = [{
label: "All Videos",
content: (
<div className="searcher-container in-modal m-4" id="allVideos">
<VideoTabsContent debug={debug} onSelect={onSelect}/>
<VideoTabsContent
debug={debug}
onSelect={(data)=>{
onSelect(data)
setOnSelectTimeStamp(new Date());
}}
/>
</div>
)
}]
@@ -45,6 +52,7 @@ export default memo(function SelectModalWrapper({
modalTitle={modalTitle}
label={inputLabel}
value={value}
forcedClose={onSelectTimeStamp}
onClear={()=>{onSelect({title:"",token:""})}}
>
<NonRouteTabs tabs={tabs} />
@@ -0,0 +1,179 @@
@import "scss/colours.scss";
.selection-bubbles-wrapper {
position: relative;
overflow: hidden;
transition: all 0.4s;
.scroll-indicator {
position: absolute;
z-index: 3;
top: 50%;
right: 0px;
transform: translateY(-50%);
color: rgba($white, 0.3);
pointer-events: none;
transition: all 0.4s;
}
.scroll-indicator-left {
position: absolute;
z-index: 3;
top: 50%;
left: 0px;
transform: translateY(-50%);
color: rgba($white, 0.3);
pointer-events: none;
transition: all 0.4s;
}
&:after {
content: "";
position: absolute;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
width: 60px;
background: linear-gradient(to right, rgba($panel, 0), $panel);
pointer-events: none;
display: none;
transition: all 0.4s;
}
&:before {
content: "";
position: absolute;
z-index: 2;
top: 0;
left: 0;
bottom: 0;
width: 60px;
background: linear-gradient(to left, rgba($panel, 0), $panel);
pointer-events: none;
display: none;
transition: all 0.4s;
}
&.show-gradient:after {
display: block;
}
&.show-left-gradient:before {
display: block;
}
}
.selection-bubbles {
display: flex;
gap: 0.5rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
@media (min-width: 768px) {
flex-wrap: wrap;
justify-content: start;
overflow-x: visible;
}
&.no-side-scroll {
flex-wrap: wrap;
}
}
.bubble-button {
flex: 0 0 auto;
border: 2px solid darken($secondary, 3%);
padding: 8px 16px;
border-radius: 0.5rem;
background-color: darken($secondary, 3%);
cursor: pointer;
outline: none;
transition: all 0.2s ease;
font-size: 15px;
font-weight: 500;
color: rgba($white, 0.75);
white-space: nowrap;
&.bubble-card {
position: relative;
overflow: hidden;
display: flex;
width: 100%;
height: 100%;
aspect-ratio: 1 / 1;
padding: 0;
.bubble-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.bubble-text {
position: absolute;
z-index: 3;
color: white;
padding-left: 10px;
padding-bottom: 5px;
text-align: left;
width: 100%;
bottom: 0;
left: 0;
font-size: 14px;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
z-index: 2;
}
&.selected::after {
background: linear-gradient(
to top,
rgba(desaturate(darken($primary, 20%), 30%), 70%),
transparent
);
}
}
&:hover {
background-color: $secondary;
border-color: $secondary;
&.fill {
border-color: $primary;
background-color: $primary;
}
}
&.selected {
color: $white;
border-color: $primary;
background-color: lighten($secondary, 6%);
&.fill {
background-color: $primary;
}
}
}
@media (max-width: 768px) {
.mobile-flex-nowrap {
flex-wrap: nowrap;
}
}
@@ -0,0 +1,159 @@
import React, { useState, useRef, useEffect } from "react";
import "./SelectionBubblesV2.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/pro-solid-svg-icons";
interface Option {
label: string;
imageUrl?: string;
token?: string;
[key: string]: any;
}
interface SelectionBubbleProps {
options: Option[];
onSelect: (selected: any) => void;
selectedStyle?: "outline" | "fill";
mobileSideScroll?: boolean;
variant?: "default" | "card";
}
export default function SelectionBubblesV2({
options,
onSelect,
selectedStyle = "outline",
mobileSideScroll = false,
variant = "default",
}: SelectionBubbleProps) {
const [selectedOption, setSelectedOption] = useState<string | null>(
options.length > 0 ? options[0].label : null
);
const [showGradient, setShowGradient] = useState(true);
const [showLeftGradient, setShowLeftGradient] = useState(false);
const bubblesRef = useRef<HTMLDivElement>(null);
const handleSelect = (
event: React.MouseEvent<HTMLButtonElement> | null,
optionLabel: string
) => {
event?.preventDefault();
setSelectedOption(optionLabel);
onSelect(optionLabel);
};
const handleScroll = () => {
if (bubblesRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = bubblesRef.current;
const isAtEnd = scrollWidth - Math.round(scrollLeft + clientWidth) <= 1;
const isAtStart = scrollLeft <= 1;
const isMobile = window.innerWidth <= 768;
setShowGradient(!isAtEnd && isMobile);
setShowLeftGradient(!isAtStart && isMobile);
}
};
useEffect(() => {
if (bubblesRef.current) {
bubblesRef.current.addEventListener("scroll", handleScroll);
handleScroll();
return () =>
// eslint-disable-next-line react-hooks/exhaustive-deps
bubblesRef.current?.removeEventListener("scroll", handleScroll);
}
}, [options]);
useEffect(() => {
const handleResize = () => handleScroll();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (options.length > 0 && selectedOption !== null) {
onSelect(options[0].label);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div
className={`selection-bubbles-wrapper ${
showGradient && mobileSideScroll ? "show-gradient" : ""
} ${
showLeftGradient && mobileSideScroll ? "show-left-gradient" : ""
}`.trim()}
>
{showLeftGradient && mobileSideScroll && (
<FontAwesomeIcon
icon={faChevronLeft}
className="scroll-indicator-left fs-5"
/>
)}
<div
className={`selection-bubbles ${
!mobileSideScroll ? "no-side-scroll" : ""
}`.trim()}
ref={bubblesRef}
>
{variant === "default" ? (
<>
{options.map(({ label }) => (
<button
key={label}
className={`bubble-button ${
selectedOption === label
? `selected ${selectedStyle === "fill" ? "fill" : ""}`
: ""
}`.trim()}
onClick={event => handleSelect(event, label)}
>
{label}
</button>
))}
</>
) : (
<div className="w-100">
<div
className={`row g-2 ${
mobileSideScroll ? "mobile-flex-nowrap" : ""
}`}
>
{options.map(({ label, imageUrl }) => (
<div className="col-4 col-lg-3">
<button
key={label}
className={`bubble-button ${
selectedOption === label
? `selected ${selectedStyle === "fill" ? "fill" : ""}`
: ""
} ${variant === "card" ? "bubble-card" : ""}`.trim()}
onClick={event => handleSelect(event, label)}
>
{variant === "card" && imageUrl && (
<img
src={imageUrl}
alt={label}
className="bubble-image"
draggable="false"
/>
)}
<span className="bubble-text">{label}</span>
</button>
</div>
))}
</div>
</div>
)}
</div>
{showGradient && mobileSideScroll && (
<FontAwesomeIcon
icon={faChevronRight}
className="scroll-indicator fs-5"
/>
)}
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./SelectionBubblesV2";
@@ -0,0 +1,44 @@
import React from "react";
import { PopupButton } from "@typeform/embed-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faPartyHorn } from "@fortawesome/pro-solid-svg-icons";
import { get, set } from "local-storage";
import Button from "../Button";
interface TypeformButtonProps {
formId: string;
label: string;
labelSubmitted: string;
}
export default function TypeformButton({
formId,
label,
labelSubmitted,
}: TypeformButtonProps) {
return (
<>
{!get<boolean>("isSubmit") ? (
<PopupButton
id={formId}
className="button button-primary button-small"
onSubmit={() => {
set<boolean>("isSubmit", true);
console.log("Form submitted!");
}}
>
{label}
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
</PopupButton>
) : (
<Button
label={labelSubmitted}
small={true}
disabled={true}
variant="secondary"
icon={faPartyHorn}
/>
)}
</>
);
}
@@ -0,0 +1 @@
export { default } from "./TypeformButton";
@@ -0,0 +1,11 @@
.typeformcontainer {
height: 50vh; // 50% of the viewport height
@media (min-width: 768px) { // Tablets and above
height: 60vh;
}
@media (min-width: 1024px) { // Desktops and above
height: 400px; // Fixed height for large screens
}
}
@@ -0,0 +1,30 @@
import React from "react";
import { Widget } from "@typeform/embed-react";
import { get, set } from "local-storage";
import "./TypeformContainer.scss";
interface TypeformWidgetProps {
formId: string;
}
export default function TypeformWidget({ formId }: TypeformWidgetProps) {
return (
<>
{!get<boolean>("isSubmit") ? (
<div className="typeformcontainer">
<Widget
id={formId}
style={{ width: "100%", height: "100%" }}
opacity={80}
onSubmit={() => {
set<boolean>("isSubmit", true);
console.log("Form submitted!");
}}
hideHeaders
enableSandbox={false}
/>
</div>
) : null}
</>
);
}
@@ -0,0 +1 @@
export { default } from "./TypeformWidget";
@@ -1,14 +1,22 @@
import React, { useState, forwardRef } from "react";
import React, {
useState,
forwardRef,
memo,
useEffect,
} from "react";
import { useMedia } from "hooks";
import { MediaFileType } from "@storyteller/components/src/api";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import makeClass from "resources/makeClass";
import { Label } from "components/common";
import { Label, Spinner } from "components/common";
import './styles.scss';
export interface VideoFakeyouProps{
width?: number|string;
height?: number|string;
hideIfNoMedia?: boolean;
wrapperClassName?: string;
controls?:boolean;
muted?:boolean;
@@ -21,9 +29,12 @@ export interface VideoFakeyouProps{
type Ref = HTMLVideoElement;
const VideoFakeyou = forwardRef<Ref, VideoFakeyouProps>(({
const VideoFakeyou = memo(forwardRef<Ref, VideoFakeyouProps>(({
width,
height,
hideIfNoMedia = false,
wrapperClassName,
controls,
controls = true,
muted,
className,
src,
@@ -32,7 +43,12 @@ const VideoFakeyou = forwardRef<Ref, VideoFakeyouProps>(({
onResponse,
...rest
}: VideoFakeyouProps, ref) => {
const [mediaFile, setMediaFile] = useState<MediaFileType>();
//console.log(`Video Player rerender: ${mediaToken}`);
const [mediaFile, setMediaFile] = useState<MediaFileType|null>(null);
useEffect(()=>{
setMediaFile(null);
},[mediaToken])
useMedia({
mediaToken: mediaToken,
onSuccess: (res: any) => {
@@ -42,10 +58,20 @@ const VideoFakeyou = forwardRef<Ref, VideoFakeyouProps>(({
});
const mediaLink = src || (mediaFile && new BucketConfig().getGcsUrl(mediaFile.public_bucket_path));
const sizing = {
height: typeof height === "number" ? height+'px'
: typeof height === "string" ? height
:"auto",
width: typeof width === "number" ? width+'px'
: typeof width === "string" ? width
:"auto",
}
if (mediaLink){
return (
<div {...{ ...makeClass("fy-video",wrapperClassName) }}>
<div
style={sizing}
{...{ ...makeClass("fy-video",wrapperClassName) }}
>
{label && <Label label={label}/>}
<video
controls={controls}
@@ -60,9 +86,17 @@ const VideoFakeyou = forwardRef<Ref, VideoFakeyouProps>(({
<source src={mediaLink} type="video/mp4" />
</video>
</div>
)
}
return null;
});
);
}else if (!hideIfNoMedia){
return(
<div
style={sizing}
{...{ ...makeClass("fy-video",wrapperClassName) }}
>
<Spinner/>
</div>
);
} return null;
}));
export default VideoFakeyou;
@@ -5,7 +5,15 @@
width: 100%;
margin: 0;
position: relative;
background-color: $bg;
border-radius: 0.5rem;
border: $panel-inner-border 1px solid;
.fy-spinner{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.fit-content {
height: fit-content;
}
@@ -1,47 +1,99 @@
import React, {
useRef,
// useState
useState,
PointerEvent
} from 'react';
import {
faGripDots
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { QuickTrimData } from './types';
export default function TrimScrubber({
width
trimStart,
trimDuration,
duration,
width,
boundingWidth,
onChange
}:{
width:number
trimStart:number;
trimDuration: number;
duration: number;
boundingWidth: number;
width:number;
onChange:(val:QuickTrimData)=>void
}){
const ref = useRef<HTMLDivElement>(null);
const [{
left,
leftOffset,
pointerStart
}, setState] = useState<{
left: number,
leftOffset:number
pointerStart:number
}>({
left: trimStart/duration*boundingWidth,
leftOffset: trimStart/duration*boundingWidth,
pointerStart: -1
});
const ref = useRef<HTMLDivElement | null>(null);
if(ref.current){
ref.current.style.cursor = pointerStart >= 0
? 'grabbing' : 'grab';
}
function handleScrubStart(e: PointerEvent<HTMLDivElement>){
e.persist();
// console.log(`start: ${pointerStart}` );
setState((curr)=>({
...curr,
pointerStart: e.clientX
}));
}
function handleScrubEnd(e: PointerEvent<HTMLDivElement>){
e.persist();
// console.log(`end` );
setState((curr)=>({
...curr,
pointerStart: -1,
leftOffset: left,
}));
//console.log(`${duration} * ${left} / ${boundingWidth} = ${duration * left / boundingWidth}`)
onChange(({
trimStartSeconds: duration * left / boundingWidth,
trimEndSeconds: duration * left / boundingWidth + trimDuration,
}))
}
function handleScrubMove(e: PointerEvent<HTMLDivElement>){
e.persist();
if(pointerStart >= 0 && pointerStart!==null && ref.current){
// console.log(`${e.clientX} - ${pointerStart} = ${e.clientX-pointerStart}`)
let newLeft = leftOffset + e.clientX - pointerStart;
// console.log(`${newLeft} + ${boundingWidth * widthPercent/100} > ${boundingWidth}`)
if (newLeft + width > boundingWidth) {
newLeft = boundingWidth - width;
}else if(newLeft < 0) {
newLeft = 0;
}
setState((curr)=>({
...curr,
left: newLeft
}));
}
}
return(
<div className="trim-scrubber"
ref={ref}
style={{width: width + "%"}}
// onPointerDown={()=>{
// if(trimZoneRef.current){
// trimZoneRef.current.style.cursor = 'grabbing';
// setTrimState((curr)=>({
// ...curr,
// isScrubbingTrim: true,
// }))
// }
// }}
// onPointerUp={()=>{
// if(trimZoneRef.current) {
// trimZoneRef.current.style.cursor = 'grab';
// setTrimState((curr)=>({
// ...curr,
// isScrubbingTrim: false,
// }));
// }
// }}
// onPointerMove={(e: PointerEvent<HTMLDivElement>)=>{
// if(trimZoneRef.current && playbarRef.current && isScrubbingTrim){
// // console.log(fractionToPercentage(
// // (e.clientX - trimZoneRef.current.getBoundingClientRect().left) / (playbarRef.current.getBoundingClientRect().width)
// // ) + "%");
// }
// }}
style={{
width: width + "px",
left: left,
}}
onPointerDown={handleScrubStart}
onPointerUp={handleScrubEnd}
onPointerLeave={handleScrubEnd}
onPointerMove={handleScrubMove}
>
<FontAwesomeIcon icon={faGripDots} />
</div>
@@ -4,7 +4,6 @@ import React, {
useLayoutEffect,
useState,
useRef,
// PointerEvent,
} from "react";
import {
faArrowsRepeat,
@@ -35,8 +34,8 @@ const trimOptions: { [key: string]: number } = {
const initialTrimState: TrimStates = {
canNotTrim: true,
isScrubbingTrim: false,
trimDuration: 0,
trimReset: new Date(),
trimStart: 0,
trimEnd: 0,
maxDuration: 0,
@@ -65,16 +64,17 @@ export default memo(function VideoQuickTrim({
const [isRepeatOn, setIsRepatOn] = useState<boolean>(true);
const [{
canNotTrim,
trimReset,
trimStart,
trimEnd,
trimDuration,
// isScrubbingTrim,
maxDuration
}, setTrimState] = useState<TrimStates>(initialTrimState);
const videoRefCallback = useCallback(node => {
console.log("videoRefCallback");
//console.log("videoRefCallback");
if (node !== null) {
// DOM node referenced by ref has changed and exists
videoRef.current = node;
@@ -98,8 +98,10 @@ export default memo(function VideoQuickTrim({
};
node.ontimeupdate = ()=>{
if(isRepeatOn && node.currentTime >= trimEnd){
node.currentTime = 0;
if(isRepeatOn &&
(node.currentTime >= trimEnd || node.currentTime <= trimStart)
){
node.currentTime = trimStart;
}
setPlaybarState((curr)=>({
...curr,
@@ -113,7 +115,7 @@ export default memo(function VideoQuickTrim({
node.onended = ()=>{setPlaypause("ended");};
} // else{} DOM node referenced by ref has been unmounted
}, [playbarWidth, onSelect, isRepeatOn, trimEnd]); //END videoRefCallback
}, [playbarWidth, onSelect, isRepeatOn, trimStart, trimEnd]); //END videoRefCallback
function handleWindowResize() {
if (playbarRef.current !== null) {
@@ -142,22 +144,29 @@ export default memo(function VideoQuickTrim({
}, []);
const handleChangeTrimDuration = (selected: string) =>{
if(!canNotTrim){
const newTrimEnd = trimStart+ trimOptions[selected];
if(!canNotTrim && maxDuration > 0 && trimOptions[selected] <= maxDuration){
let newTrimStart = trimStart;
let newTrimEnd = trimStart+ trimOptions[selected];
if (newTrimEnd > maxDuration){
newTrimEnd = maxDuration;
newTrimStart = maxDuration - trimOptions[selected];
}
setTrimState((curr)=>({
...curr,
trimReset: new Date(),
trimDuration: trimOptions[selected],
trimStart: newTrimStart,
trimEnd: newTrimEnd,
}))
onSelect({
trimStartSeconds: trimStart,
trimStartSeconds: newTrimStart,
trimEndSeconds: newTrimEnd,
});
if (isRepeatOn
&& videoRef.current
&& videoRef.current.currentTime > newTrimEnd
){
videoRef.current.currentTime = trimStart;
videoRef.current.currentTime = newTrimStart;
}
}
}
@@ -170,16 +179,17 @@ export default memo(function VideoQuickTrim({
}
};
const trimScrubberWidth = videoRef.current
const trimScrubberWidth = videoRef.current && playbarWidth > 0
? trimDuration > 0 && trimDuration < videoRef.current.duration
? (trimDuration / videoRef.current.duration * 100)
: 100
? (trimDuration / videoRef.current.duration * playbarWidth)
: playbarWidth
: 0;
return (
<div className="fy-video-quicktrim">
<div className="video-wrapper">
<VideoFakeyou
height={500}
controls={false}
muted={isMuted}
ref={videoRefCallback}
@@ -212,7 +222,30 @@ export default memo(function VideoQuickTrim({
{/* END of Video Wrapper */}
<div className="playbar" ref={playbarRefCallback}>
<div className="playbar-bg" />
<TrimScrbber width={trimScrubberWidth} />
{trimScrubberWidth > 0 &&
playbarWidth > 0 &&
maxDuration > 0 &&
<TrimScrbber
key={trimReset.toString()}
boundingWidth={playbarWidth}
width={trimScrubberWidth}
trimStart={trimStart}
trimDuration={trimDuration}
duration={maxDuration}
onChange={(val: QuickTrimData)=>{
//console.log(val);
setTrimState((curr)=>({
...curr,
trimStart: val.trimStartSeconds,
trimEnd: val.trimEndSeconds
}))
onSelect({
trimStartSeconds: val.trimStartSeconds,
trimEndSeconds: val.trimEndSeconds,
});
}}
/>
}
<div className="playcursor" style={{left: timeCursorOffset+"px"}}/>
</div> {/* END of Playbar */}
@@ -15,6 +15,8 @@ $playbar-height: 2rem;
max-height: 500px;
width: 100%;
margin-bottom: -7px;
border-radius: 0.5rem 0.5rem 0 0;
border: none;
video {
border-radius: 0.5rem 0.5rem 0 0;
@@ -88,7 +90,7 @@ $playbar-height: 2rem;
outline: 0.125rem $white solid;
border-top: 1rem $white solid;
border-radius: 0.25rem 0.25rem 0.125rem 0.125rem;
// cursor: grab;
cursor: grab;
display: flex;
justify-content: center;
@@ -7,7 +7,7 @@ export type QuickTrimData = {
// INTERNALS
export type TrimStates = {
canNotTrim: boolean;
isScrubbingTrim: boolean;
trimReset: Date;
trimDuration: number;
trimStart: number;
trimEnd: number;
@@ -53,9 +53,8 @@ export default function SideNav({
const isMenuOpen = wrapper?.classList.contains("toggled");
const isLoggedIn = sessionWrapper.isLoggedIn();
const isOnLandingPage = window.location.pathname === "/";
const isOnLoginOrSignUpPage = window.location.pathname.includes(
"/login" || "/signup"
);
const isOnLoginPage = window.location.pathname.includes("/login");
const isOnSignUpPage = window.location.pathname.includes("/signup");
const isOnStudioPage = window.location.pathname.includes("/studio");
let history = useHistory();
@@ -107,7 +106,7 @@ export default function SideNav({
}, []);
const shouldNotShowSidebar =
(!isLoggedIn && (isOnLandingPage || isOnLoginOrSignUpPage)) ||
(!isLoggedIn && (isOnLandingPage || isOnLoginPage || isOnSignUpPage)) ||
isOnStudioPage;
const shouldShowSidebar = windowWidth >= 992 && !shouldNotShowSidebar;
const sidebarClassName = `sidebar ${
@@ -1,5 +1,9 @@
@import "scss/colours.scss";
.topbar-bg-transparent {
background-color: transparent;
}
.topbar-bg-dark {
background-color: $bg !important;
border-bottom: 1px solid transparent !important;
@@ -45,16 +45,16 @@ export default function TopNav({
const [menuButtonIcon, setMenuButtonIcon] = useState(faBars);
// const { t } = useLocalize("TopNav");
const isOnLandingPage = window.location.pathname === "/";
const isOnLoginOrSignUpPage = window.location.pathname.includes(
"/login" || "/signup"
);
const isOnLoginPage = window.location.pathname.includes("/login");
const isOnSignUpPage = window.location.pathname.includes("/signup");
const isOnStudioPage = window.location.pathname.includes("/studio");
const { open } = useModal();
const openModal = () => open({ component: InferenceJobsModal });
const [isScrolled, setIsScrolled] = useState(false);
const loggedIn = sessionWrapper.isLoggedIn();
const showNavItem = !loggedIn && (isOnLandingPage || isOnLoginOrSignUpPage);
const showNavItem =
!loggedIn && (isOnLandingPage || isOnLoginPage || isOnSignUpPage);
const handleMenuButtonClick = () => {
if (window.innerWidth < 1200) {
@@ -95,8 +95,15 @@ export default function TopNav({
};
useEffect(() => {
const topBarWrapper = document.getElementById("topbar-wrapper");
const handleMenuToggle = (event: any) => {
setMenuButtonIcon(event.detail.isOpen ? faXmark : faBars);
if (event.detail.isOpen) {
topBarWrapper?.classList.remove("topbar-wrapper-transparent");
} else {
topBarWrapper?.classList.add("topbar-wrapper-transparent");
}
};
window.addEventListener("menuToggle", handleMenuToggle);
@@ -185,8 +192,12 @@ export default function TopNav({
<div
id="topbar-wrapper"
className={`position-fixed ${
!loggedIn && isOnLandingPage && !isScrolled ? "topbar-bg-dark" : ""
}`}
domain.title !== "FakeYou"
? "topbar-bg-transparent"
: !loggedIn && isOnLandingPage && !isScrolled
? "topbar-bg-dark"
: ""
}`.trim()}
>
<div className="topbar-nav">
<div className="topbar-nav-left">
@@ -230,11 +241,18 @@ export default function TopNav({
<div className="topbar-nav-center">
{/* Search Bar */}
<div className="d-none d-lg-block">
{(!isOnLandingPage && !isOnLoginOrSignUpPage && !isOnStudioPage) ||
(loggedIn && !isOnLoginOrSignUpPage && !isOnStudioPage) ||
{(!isOnLandingPage &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(loggedIn &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(isOnLandingPage &&
isScrolled &&
!isOnLoginOrSignUpPage &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ? (
<SearchBar
onFocus={onFocusHandler}
@@ -49,7 +49,7 @@ export default function SessionProvider({
const canEditTtsModel = (userToken: string) =>
user?.canEditOtherUsersTtsModels || userTokenMatch(userToken);
const canBanUsers = () => user?.can_ban_users || false;
const studioAccessCheck = (content: React.ElementType) => !user?.can_access_studio ? <StudioNotAvailable /> : content;
const studioAccessCheck = (content: React.ElementType) => !sessionWrapper?.canAccessStudio() ? <StudioNotAvailable /> : content;
return (
<SessionContext.Provider
+9 -1
View File
@@ -61,6 +61,10 @@ $topbar-height: 66px;
padding-left: 260px;
padding-top: $topbar-height;
transition: all 0.2s ease;
&.no-page-padding-top {
padding-top: 0;
}
}
/* Sidebar Styles */
@@ -171,7 +175,11 @@ $topbar-height: 66px;
background: $panel;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
z-index: 1000;
transition: all 0.1s ease;
transition: 0.15s;
&.topbar-wrapper-transparent {
background: none;
}
}
.topbar-nav {
+28 -20
View File
@@ -1,18 +1,24 @@
type DomainConfig = Record<string, any>;
export enum Website {
FakeYou,
StorytellerAi,
}
export interface DomainConfig {
website: Website,
logo: string,
title: string,
link: string,
}
const domainConfigs: Record<string, DomainConfig> = {
"storyteller.ai": {
logo: "/fakeyou/Storyteller-Logo-5.png",
"storyteller": {
website: Website.StorytellerAi,
logo: "/fakeyou/Storyteller-Logo-1.png",
title: "Storyteller AI",
link: "https://storyteller.ai",
},
"fakeyou.com": {
logo: "/fakeyou/FakeYou-Logo-2.png",
title: "FakeYou",
link: "https://fakeyou.com",
},
// Default to FakeYou.com
default: {
"fakeyou": {
website: Website.FakeYou,
logo: "/fakeyou/FakeYou-Logo-2.png",
title: "FakeYou",
link: "https://fakeyou.com",
@@ -20,16 +26,18 @@ const domainConfigs: Record<string, DomainConfig> = {
};
export const getCurrentDomainConfig = (): DomainConfig => {
const currentHostname = window.location.hostname;
// Fast resolve without leaking domain details
switch (window.location.hostname) {
case "fakeyou.com":
return domainConfigs.fakeyou;
case "storyteller.ai":
return domainConfigs.storyteller;
}
const matchingConfigKey = Object.keys(domainConfigs).find(
key => currentHostname === key || currentHostname.endsWith(`.${key}`)
);
if (window.location.hostname.includes("storyteller")) {
return domainConfigs.storyteller;
}
const domainConfig =
matchingConfigKey !== undefined
? domainConfigs[matchingConfigKey]
: domainConfigs["default"];
return domainConfig;
// Default fallback
return domainConfigs.fakeyou;
};
@@ -0,0 +1,19 @@
import { DiscordLink2 } from "@storyteller/components/src/elements/DiscordLink2";
import { Container } from "components/common";
import PageHeader from "components/layout/PageHeader";
import React from "react";
export function VideoStyleTransferNotAvailable() {
const subText = (
<> Please visit our <DiscordLink2 /> to join the wait list. </>
);
return (
<Container type="panel" className="mb-5">
<PageHeader
title={"Video Style Transfer Features Not Available"}
subText={subText}
/>
</Container>
);
}
@@ -127,7 +127,7 @@ export default function Dashboard(props: DashboardProps) {
];
return (
<Panel clear={true} className="mt-5">
<Panel clear={true} className={`${!isLoggedIn ? "section" : "mt-5"}`}>
<h1 className="fw-bold mb-4">{t("productsTitle")}</h1>
<DashboardRow items={dashboardProducts} />
@@ -7,15 +7,19 @@ import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClie
import { Container } from "components/common";
import FakeYouLandingHeader from "./fakeyou/FakeYouLandingHeader";
import Dashboard from "./Dashboard";
import FakeYouLandingBody from "./fakeyou/FakeYouLandingBody";
import { useDomainConfig } from "context/DomainConfigContext";
import StorytellerLanding from "./storyteller/StorytellerLanding";
import LandingVideoReel from "./components/LandingVideoReel";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
import "./LandingPage.scss";
// import VstSectionV1 from "./components/VstSectionV1";
import VstSectionV2 from "./components/VstSectionV2";
import FakeYouLandingBody from "./fakeyou/FakeYouLandingBody";
import { DomainConfig, Website } from "utils/domainConfig";
interface Props {
sessionWrapper: SessionWrapper;
@@ -32,27 +36,36 @@ interface Props {
function LandingPage(props: Props) {
usePrefixedDocumentTitle("FakeYou Celebrity Voice Generator");
PosthogClient.recordPageview();
const domain = useDomainConfig();
const domain : DomainConfig = useDomainConfig();
const isLoggedIn = props.sessionWrapper.isLoggedIn();
return (
<>
{domain.website === Website.StorytellerAi && !isLoggedIn && <LandingVideoReel />}
<Container type="panel">
{domain.title === "FakeYou" ? (
{domain.website === Website.FakeYou ? (
<>
{/* FAKEYOU.COM */}
{!isLoggedIn && (
<FakeYouLandingHeader
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
inferenceJobs={props.inferenceJobs}
ttsInferenceJobs={props.ttsInferenceJobs}
enqueueInferenceJob={props.enqueueInferenceJob}
inferenceJobsByCategory={props.inferenceJobsByCategory}
enqueueTtsJob={props.enqueueTtsJob}
/>
<>
<FakeYouLandingHeader
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={
props.sessionSubscriptionsWrapper
}
inferenceJobs={props.inferenceJobs}
ttsInferenceJobs={props.ttsInferenceJobs}
enqueueInferenceJob={props.enqueueInferenceJob}
inferenceJobsByCategory={props.inferenceJobsByCategory}
enqueueTtsJob={props.enqueueTtsJob}
/>
{/* <VstSectionV1 /> */}
<VstSectionV2 />
</>
)}
<Dashboard sessionWrapper={props.sessionWrapper} />
@@ -63,7 +76,15 @@ function LandingPage(props: Props) {
<>
{/* STORYTELLER,AI */}
{!isLoggedIn ? (
<StorytellerLanding />
<StorytellerLanding
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
inferenceJobs={props.inferenceJobs}
ttsInferenceJobs={props.ttsInferenceJobs}
enqueueInferenceJob={props.enqueueInferenceJob}
inferenceJobsByCategory={props.inferenceJobsByCategory}
enqueueTtsJob={props.enqueueTtsJob}
/>
) : (
<Dashboard sessionWrapper={props.sessionWrapper} />
)}
@@ -0,0 +1,69 @@
import { Button } from "components/common";
import React, { useEffect } from "react";
interface LandingVideoReelProps {}
export default function LandingVideoReel(props: LandingVideoReelProps) {
const isOnLandingPage = window.location.pathname === "/";
useEffect(() => {
const contentWrapper = document.getElementById("page-content-wrapper");
const topBarWrapper = document.getElementById("topbar-wrapper");
const searchBar = document.querySelector(".search-field .form-control");
const handleScroll = () => {
if (window.scrollY > 100) {
topBarWrapper?.classList.remove("topbar-wrapper-transparent");
searchBar?.classList.remove("landing-search");
} else {
topBarWrapper?.classList.add("topbar-wrapper-transparent");
searchBar?.classList.add("landing-search");
}
};
if (isOnLandingPage) {
contentWrapper?.classList.add("no-page-padding-top");
topBarWrapper?.classList.add("topbar-wrapper-transparent");
searchBar?.classList.add("landing-search");
window.addEventListener("scroll", handleScroll);
} else {
contentWrapper?.classList.remove("no-page-padding-top");
topBarWrapper?.classList.remove("topbar-wrapper-transparent");
searchBar?.classList.remove("landing-search");
window.removeEventListener("scroll", handleScroll);
}
return () => {
window.removeEventListener("scroll", handleScroll);
topBarWrapper?.classList.remove("topbar-wrapper-transparent");
searchBar?.classList.remove("landing-search");
contentWrapper?.classList.remove("no-page-padding-top");
};
}, [isOnLandingPage]);
return (
<div className="vh-100 d-flex align-items-center justify-content-center position-relative">
<video
autoPlay
muted
loop
className="position-absolute w-100 h-100"
style={{ objectFit: "cover" }}
>
<source src="/videos/landing-reel.mp4" type="video/mp4" />
</video>
<div
className="position-absolute w-100 h-100"
style={{ backgroundColor: "rgba(20, 20, 27, 0.6)", zIndex: 0 }}
/>
<div className="text-center text-white d-flex flex-column align-items-center zi-2">
<h1 className="display-2 fw-bolder">Enabling Anyone to Make Professional Film, Animation, and Music</h1>
<h5 className="fw-normal mb-3 mt-2 w-75 lh-base opacity-75">
StorytellerAI's powerful new engine combines Generative AI and
User Generated Content to radically democratize both audio and video production.
</h5>
<Button label="Sign Up Now" to="/signup" className="mt-5" />
</div>
</div>
);
}
@@ -0,0 +1,84 @@
@import "scss/colours.scss";
.vst-section {
padding-top: 5rem;
padding-bottom: 5rem;
}
.vst-sbs-panel {
background-color: $panel;
height: 400px;
overflow: hidden;
border: lighten($panel, 8%) solid 2px;
border-radius: 0.5rem;
overflow: hidden;
}
.vst-sbs-video-cover {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
z-index: 1;
}
.vst-sbs-video-overlay-container {
position: relative;
height: 100%;
}
.vst-sbs-video-overlay-text {
position: absolute;
z-index: 3;
top: 0;
left: 0;
color: white;
padding: 0.5rem;
}
.vst-sbs-video-overlay-cta {
position: absolute;
z-index: 3;
bottom: 0;
right: 0;
color: $white;
font-weight: 500;
padding: 0.5rem;
.fy-badge {
background-color: rgba(
darken(desaturate($primary, 25%), 10%),
60%
) !important;
border: 2px solid rgba($primary, 60%) !important;
font-size: 12px;
padding: 0.12rem 0.36rem;
&:hover {
background-color: darken(desaturate($primary, 25%), 10%) !important;
border-color: darken(desaturate($primary, 25%), 10%) !important;
}
}
}
@media (max-width: 992px) {
.vst-sbs-panel {
height: 400px;
}
.vst-sbs-video-overlay-cta {
padding: 0.25rem;
.fy-badge {
border-radius: 0.3rem;
border: 1px solid rgba($primary, 60%) !important;
font-size: 10px;
}
}
}
@media (max-width: 768px) {
.vst-sbs-panel {
height: 240px;
}
}
@@ -0,0 +1,102 @@
import { Badge, Button, Panel } from "components/common";
import React, { useEffect, useRef, useState } from "react";
import "./VstSectionV1.scss";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
// import { Link } from "react-router-dom";
interface VstSectionV1Props {}
const columns = [
{
title: "Original Video",
videoSrc: "/videos/landing/vst/1-original.mp4",
ctaLink: "/video-styletransfer",
},
{
title: "2D Flat Anime",
videoSrc: "/videos/landing/vst/1-2d_flat_anime.mp4",
ctaLink: "/video-styletransfer",
},
{
title: "Ink B&W",
videoSrc: "/videos/landing/vst/1-ink_bw.mp4",
ctaLink: "/video-styletransfer",
},
{
title: "Origami",
videoSrc: "/videos/landing/vst/1-origami.mp4",
ctaLink: "/video-styletransfer",
},
];
export default function VstSectionV1(props: VstSectionV1Props) {
const videosRef = useRef<HTMLVideoElement[]>([]);
const [loadedVideos, setLoadedVideos] = useState(0);
useEffect(() => {
const checkAllVideosLoaded = () => {
setLoadedVideos(prevLoaded => prevLoaded + 1);
};
videosRef.current.forEach(video => {
video.addEventListener("canplaythrough", checkAllVideosLoaded);
});
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
videosRef.current.forEach(video => {
video.removeEventListener("canplaythrough", checkAllVideosLoaded);
});
};
}, []);
useEffect(() => {
if (loadedVideos === columns.length) {
videosRef.current.forEach(video => video.play());
}
}, [loadedVideos]);
return (
<Panel clear={true} className="vst-section">
<div className="col-12 col-lg-6 mb-5 pb-lg-3">
<h1 className="fw-bold">Video Style Transfer</h1>
<p className="opacity-75 mb-4">
Transform your videos effortlessly with video style transfer. Apply
unique styles and effects to create visually captivating content.
</p>
<div className="d-flex">
<Button
label="Try Video Style Transfer"
small={true}
icon={faArrowRight}
iconFlip={true}
to="/video-styletransfer"
/>
</div>
</div>
<div className="row g-3 g-lg-4">
{columns.map((column, index) => (
<div className={`col-6 col-lg-3 column-${index + 1}`} key={index}>
<div className="vst-sbs-panel">
<div className="vst-sbs-video-overlay-container">
<video className="vst-sbs-video-cover" autoPlay loop muted>
<source src={column.videoSrc} type="video/mp4" />
Your browser does not support the video tag.
</video>
<div className="vst-sbs-video-overlay-text">
<Badge label={column.title} color="gray" />
</div>
{/* <div className="vst-sbs-video-overlay-cta">
<Link to={column.ctaLink || "/video-styletransfer"}>
<Badge label="Try this style" />
</Link>
</div> */}
</div>
</div>
</div>
))}
</div>
</Panel>
);
}
@@ -0,0 +1 @@
export { default } from "./VstSectionV1";
@@ -0,0 +1,169 @@
@import "scss/colours.scss";
.vst-section {
padding-top: 5rem;
padding-bottom: 5rem;
}
.vst-carousel-panel {
background-color: $panel;
overflow: hidden;
border: lighten($panel, 8%) solid 2px;
border-radius: 0.5rem;
overflow: hidden;
position: relative;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.vst-styled-videos-panel {
width: 100%;
height: 100%;
background-color: $panel;
overflow: hidden;
border: lighten($panel, 8%) solid 2px;
border-radius: 0.5rem;
overflow: hidden;
position: relative;
display: flex;
flex-wrap: wrap;
> div {
border-left: lighten($panel, 8%) solid 2px;
border-top: lighten($panel, 8%) solid 2px;
box-sizing: border-box;
}
> div:nth-child(-n + 2) {
border-top: none;
}
> div:nth-child(2n + 1) {
border-left: none;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.vst-styled-videos-panel-overlay-text {
position: absolute;
z-index: 3;
top: 0;
left: 0;
color: white;
padding: 0.5rem;
display: flex;
height: auto;
}
.vst-styled-videos-panel-overlay-cta {
position: absolute;
z-index: 3;
bottom: 0;
right: 0;
color: $primary;
padding: 0.5rem;
display: flex;
height: auto;
align-items: flex-end;
justify-content: flex-end;
margin: 0;
.fy-badge {
background-color: rgba(
darken(desaturate($primary, 25%), 10%),
40%
) !important;
border: 2px solid rgba($primary, 60%) !important;
font-size: 12px;
padding: 0.12rem 0.36rem;
&:hover {
background-color: darken(desaturate($primary, 25%), 10%) !important;
border-color: darken(desaturate($primary, 25%), 10%) !important;
}
}
}
.swiper {
width: 100%;
margin: 0;
margin-left: auto;
margin-right: auto;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
&.vst-swiper-main {
height: auto !important;
border-radius: 0.5rem;
overflow: hidden;
}
&.vst-swiper-thumbs {
height: auto !important;
.swiper-slide {
opacity: 0.4;
cursor: pointer;
transition: 0.2s;
.vst-carousel-panel {
border: lighten($panel, 15%) solid 2px;
}
&:hover {
opacity: 0.75;
}
}
.swiper-slide-thumb-active {
opacity: 1 !important;
.vst-carousel-panel {
border: $primary solid 2px;
}
}
}
}
.swiper-slide {
background-size: contain;
background-position: center;
}
.vst-swiper-main-overlay-text {
position: absolute;
z-index: 3;
top: 0;
left: 0;
color: white;
padding: 0.75rem;
}
@media (max-width: 992px) {
.vst-styled-videos-panel-overlay-cta {
padding: 0.25rem;
.fy-badge {
border-radius: 0.3rem;
border: 1px solid rgba($primary, 60%) !important;
font-size: 9px;
}
}
.vst-styled-videos-panel-overlay-text {
.fy-badge {
font-size: 11px;
}
}
}
@@ -0,0 +1,246 @@
import { Badge, Panel } from "components/common";
import React, { useEffect, useRef, useState } from "react";
import "./VstSectionV2.scss";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/thumbs";
import { Navigation, Thumbs } from "swiper/modules";
import { Swiper as SwiperType } from "swiper/types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowDownLong,
faArrowRightLong,
} from "@fortawesome/pro-solid-svg-icons";
import TypeformButton from "components/common/TypeformButton";
// import { Link } from "react-router-dom";
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 togglePlayback = () => {
// setIsPlaying(!isPlaying);
// };
const handleVideoPlay = () => {
if (!isPlaying) {
setIsPlaying(true);
}
};
const handleVideoPause = () => {
if (isPlaying) {
setIsPlaying(false);
}
};
const videoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
}
}, [isPlaying]);
const videoSet = [
{
src: "/videos/landing/vst/1-original.mp4",
styledVideos: [
{
src: "/videos/landing/vst/1-2d_flat_anime.mp4",
label: "2D Flat Anime",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/1-3d_cartoon.mp4",
label: "3D Cartoon",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/1-ink_bw.mp4",
label: "Ink B&W",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/1-origami.mp4",
label: "Origami",
ctaLink: "/video-styletransfer",
},
],
},
{
src: "/videos/landing/vst/2-original.mp4",
styledVideos: [
{
src: "/videos/landing/vst/2-2d_flat_anime.mp4",
label: "2D Flat Anime",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/2-pop_art.mp4",
label: "Pop Art",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/2-ink_splash.mp4",
label: "Ink Splash",
ctaLink: "/video-styletransfer",
},
{
src: "/videos/landing/vst/2-origami.mp4",
label: "Origami",
ctaLink: "/video-styletransfer",
},
],
},
];
const [currentStyledVideos, setCurrentStyledVideos] = useState(
videoSet[0].styledVideos
);
return (
<Panel clear={true} className="vst-section">
<div className="col-12 col-lg-6 mb-5 mb-lg-3 pb-lg-3">
<h1 className="fw-bold">Video Style Transfer</h1>
<p className="opacity-75 mb-4">
Transform your videos effortlessly with video style transfer. Apply
unique styles and effects to create visually captivating content.
</p>
<div className="d-flex">
{/* <Button
onClick={openTypeform}
label="Join Waitlist for Video Style Transfer"
small={true}
icon={faArrowRight}
iconFlip={true}
/> */}
<TypeformButton
label="Join the Waitlist"
formId="oWnV91Z9"
labelSubmitted="You're on the waitlist!"
/>
</div>
</div>
<div className="row gx-0">
<div className="col-12 col-lg-4 d-flex gap-3 justify-content-center flex-column-reverse flex-lg-column">
<Swiper
loop={false}
spaceBetween={10}
thumbs={{ swiper: thumbsSwiper }}
modules={[Navigation, Thumbs]}
className="vst-swiper-main"
onSlideChange={(swiper: SwiperType) => {
setCurrentStyledVideos(videoSet[swiper.activeIndex].styledVideos);
setActiveIndex(swiper.activeIndex);
}}
>
<div className="vst-swiper-main-overlay-text">
<Badge label="Original Video" color="gray" />
</div>
{videoSet.map((video, index) => (
<SwiperSlide key={index}>
<div className="ratio ratio-16x9 vst-carousel-panel">
<video
autoPlay
loop
muted
key={`${index}-${activeIndex}`}
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.
</video>
</div>
</SwiperSlide>
))}
</Swiper>
<Swiper
onSwiper={(swiper: SwiperType) => setThumbsSwiper(swiper)}
loop={false}
spaceBetween={10}
slidesPerView={3}
modules={[Navigation, Thumbs]}
className="vst-swiper-thumbs"
>
{videoSet.map((video, index) => (
<SwiperSlide key={index}>
<div className="ratio ratio-16x9 vst-carousel-panel">
<video loop muted>
<source src={video.src} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
<div className="col-12 col-lg-1 p-3 p-lg-0 d-flex align-items-center justify-content-center">
<FontAwesomeIcon
icon={faArrowRightLong}
className="display-5 opacity-75 d-none d-lg-block"
/>
<FontAwesomeIcon
icon={faArrowDownLong}
className="display-5 opacity-75 d-block d-lg-none"
/>
</div>
<div className="col-12 col-lg-7">
<div className="vst-styled-videos-panel d-flex flex-wrap">
{currentStyledVideos.map((styledVideo, index) => (
<div
key={`${index}-${styledVideo.src}`}
className="ratio ratio-16x9 w-50"
>
<video
autoPlay
loop
muted
key={styledVideo.src}
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.
</video>
<div className="vst-styled-videos-panel-overlay-text">
<Badge label={styledVideo.label} color="gray" />
</div>
{/* <div className="vst-styled-videos-panel-overlay-cta">
<Link to={styledVideo.ctaLink || "/video-styletransfer"}>
<Badge label="Try this style" />
</Link>
</div> */}
</div>
))}
</div>
</div>
</div>
</Panel>
);
}
@@ -0,0 +1 @@
export { default } from "./VstSectionV2";
@@ -1,9 +1,9 @@
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Panel } from "components/common";
// import { Panel } from "components/common";
import { useLocalize } from "hooks";
import React from "react";
import { Link } from "react-router-dom";
// import { Link } from "react-router-dom";
interface FakeYouLandingBodyProps {}
@@ -12,7 +12,7 @@ export default function FakeYouLandingBody(props: FakeYouLandingBodyProps) {
return (
<>
<Panel clear={true}>
{/* <Panel clear={true}>
<div className="section px-md-5 px-xl-3">
<div className="row g-4 g-lg-5 flex-row-reverse">
<div className="col-12 col-md-6 col-lg-7">
@@ -48,9 +48,9 @@ export default function FakeYouLandingBody(props: FakeYouLandingBodyProps) {
</div>
</div>
</div>
</Panel>
</Panel> */}
<div className="py-4 mb-4">
<div className="section">
<div className="container text-center community-container">
<div className="panel px-4 py-5 d-flex flex-column align-items-center community-container rounded">
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
@@ -1,24 +1,50 @@
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
import {
faArrowRight,
faCompass,
faMessageDots,
faWaveformLines,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Container, Input, Panel, Select } from "components/common";
import { useLocalize } from "hooks";
import React from "react";
import { Link } from "react-router-dom";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs";
interface StorytellerLandingProps {}
interface StorytellerLandingProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobs: Array<InferenceJob>;
ttsInferenceJobs: Array<TtsInferenceJob>;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
) => void;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
enqueueTtsJob: (jobToken: string) => void;
}
export default function StorytellerLanding(props: StorytellerLandingProps) {
const { t } = useLocalize("LandingPage");
export default function StorytellerLanding({
sessionWrapper,
sessionSubscriptionsWrapper,
inferenceJobs,
ttsInferenceJobs,
enqueueInferenceJob,
inferenceJobsByCategory,
enqueueTtsJob,
}: StorytellerLandingProps) {
// const { t } = useLocalize("LandingPage");
return (
<>
<Container type="panel" className="py-5">
{/* <Container type="panel" className="py-5">
<FakeYouLandingHeader
sessionWrapper={sessionWrapper}
sessionSubscriptionsWrapper={sessionSubscriptionsWrapper}
inferenceJobs={inferenceJobs}
ttsInferenceJobs={ttsInferenceJobs}
enqueueInferenceJob={enqueueInferenceJob}
inferenceJobsByCategory={inferenceJobsByCategory}
enqueueTtsJob={enqueueTtsJob}
/>
{/* <Container type="panel" className="py-5">
<Panel clear={true} className="py-lg-5">
<div className="row g-5 g-lg-5 flex-row-reverse">
<div className="col-12 col-md-6">
@@ -65,9 +91,9 @@ export default function StorytellerLanding(props: StorytellerLandingProps) {
</div>
</div>
</Panel>
</Container>
</Container> */}
<div className="panel rounded-0 py-5 my-5">
{/* <div className="panel rounded-0 py-5 my-5">
<Container type="panel" className="py-lg-5">
<Panel clear={true}>
<div className="d-flex flex-column align-items-center rounded-0 mt-lg-4 pt-lg-4">
@@ -184,9 +210,9 @@ export default function StorytellerLanding(props: StorytellerLandingProps) {
</div>
</Panel>
</Container>
</div>
</div> */}
<Container type="panel" className="pt-5 my-5">
{/* <Container type="panel" className="pt-5 my-5">
<Panel padding={true}>
<div className="d-flex flex-column align-items-center py-5">
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
@@ -218,7 +244,7 @@ export default function StorytellerLanding(props: StorytellerLandingProps) {
className="dots-left-bottom me-3"
/>
</Panel>
</Container>
</Container> */}
</>
);
}
@@ -1,6 +1,5 @@
import React from "react";
import React, { useEffect, useState, memo } from "react";
import WaveSurfer from "wavesurfer.js";
import { useEffect, useState } from "react";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import {
faPlay,
@@ -21,7 +20,7 @@ interface MediaAudioPlayerProps {
mediaFile: MediaFile;
}
export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
const MediaAudioPlayer = memo(({ mediaFile }: MediaAudioPlayerProps) => {
let [isPlaying, setIsPlaying] = useState(false);
let [isRepeating, setIsRepeating] = useState(false);
let [playbackSpeed, setPlaybackSpeed] = useState(PlaybackSpeed.NORMAL);
@@ -29,7 +28,7 @@ export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
useEffect(() => {
const wavesurferInstance = WaveSurfer.create({
container: "#waveform", // Previousy I used 'this.ref.current' and React.createRef()
container: "#waveform",
height: 200,
responsive: true,
waveColor: "#cbcbcb",
@@ -49,11 +48,12 @@ export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
if (waveSurfer) {
waveSurfer.load(audioLink);
}
}, [waveSurfer, mediaFile]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [waveSurfer, mediaFile.public_bucket_path, BucketConfig]);
useEffect(() => {
if (waveSurfer) {
waveSurfer.unAll(); // NB: Otherwise we keep reinstalling the hooks and cause chaos
waveSurfer.unAll();
waveSurfer.on("pause", () => {
setIsPlaying(waveSurfer!.isPlaying());
});
@@ -83,7 +83,7 @@ export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
switch (playbackSpeed) {
case PlaybackSpeed.NORMAL:
nextSpeed = PlaybackSpeed.DOUBLE;
waveSurfer!.setPlaybackRate(1.5); // Okay, so a lie...
waveSurfer!.setPlaybackRate(1.5);
break;
case PlaybackSpeed.DOUBLE:
nextSpeed = PlaybackSpeed.HALF;
@@ -97,51 +97,36 @@ export default function MediaAudioPlayer({ mediaFile }: MediaAudioPlayerProps) {
setPlaybackSpeed(nextSpeed);
};
let playButtonIcon = faPlay;
if (isPlaying) {
playButtonIcon = faPause;
}
let playButtonIcon = isPlaying ? faPause : faPlay;
let repeatButtonIcon = isRepeating ? faRepeat : faArrowRight;
let speedButtonText = "1x";
switch (playbackSpeed) {
case PlaybackSpeed.NORMAL:
speedButtonText = "1x";
break;
case PlaybackSpeed.DOUBLE:
speedButtonText = "2x";
break;
case PlaybackSpeed.HALF:
speedButtonText = "0.5x";
break;
}
let speedButtonText =
playbackSpeed === PlaybackSpeed.NORMAL
? "1x"
: playbackSpeed === PlaybackSpeed.DOUBLE
? "2x"
: "0.5x";
return (
<div>
<div id="waveform"></div>
<div className="d-flex justify-content-center gap-2 mt-3">
<Button
square={true}
icon={playButtonIcon}
onClick={() => togglePlayPause()}
/>
<Button square={true} icon={playButtonIcon} onClick={togglePlayPause} />
<Button
tooltip="Toggle Repeat"
variant="secondary"
square={true}
icon={repeatButtonIcon}
onClick={() => toggleIsRepeating()}
onClick={toggleIsRepeating}
/>
<Button
tooltip="Speed"
label={speedButtonText}
variant="secondary"
onClick={() => togglePlaybackSpeed()}
onClick={togglePlaybackSpeed}
/>
</div>
</div>
);
}
});
export default MediaAudioPlayer;
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import MediaAudioComponent from "./MediaAudioComponent";
import MediaAudioPlayer from "./MediaAudioPlayer";
import MediaVideoComponent from "./MediaVideoComponent";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import Container from "components/common/Container";
@@ -64,23 +64,27 @@ export default function MediaPage() {
},
});
const batchToken = mediaFile?.maybe_batch_token;
const [images, setImages] = useState<string[]>([]);
const [images, setImages] = useState<{ url: string; token: string }[]>([]);
const bucketConfig = new BucketConfig();
const timeCreated = moment(mediaFile?.created_at || "").fromNow();
const dateCreated = moment(mediaFile?.created_at || "").format("LLL");
const [buttonLabel, setButtonLabel] = useState("Copy");
const [copyPositiveButtonText, setCopyPositiveButtonText] = useState("Copy");
const [copyNegativeButtonText, setCopyNegativeButtonText] = useState("Copy");
const [activeSlide, setActiveSlide] = useState({ url: "", token: "" });
// Inside MediaPage.tsx
useEffect(() => {
if (batchToken) {
GetMediaBatchImages(batchToken, {}, {})
.then(response => {
if (response.success) {
const imageUrls = response.results.map(result =>
bucketConfig.getGcsUrl(result.public_bucket_path)
);
setImages(imageUrls);
const mediaItems = response.results.map(result => ({
url: bucketConfig.getGcsUrl(result.public_bucket_path),
token: result.token,
}));
setImages(mediaItems);
} else {
console.error("Failed to fetch batch images");
}
@@ -111,6 +115,10 @@ export default function MediaPage() {
}
};
const handleActiveSlideChange = (image: any) => {
setActiveSlide(image);
};
const promptSection = (
<>
{prompt?.maybe_positive_prompt && (
@@ -171,7 +179,9 @@ export default function MediaPage() {
{/* Voice model name that is used to generate the audio */}
{/*<h3 className="fw-bold mb-4">[Voice Model Name]</h3> */}
<MediaAudioComponent mediaFile={mediaFile} />
<div className="w-100">
<MediaAudioPlayer mediaFile={mediaFile} />
</div>
{/* Show TTS text input if it is a TTS result */}
{mediaFile.maybe_text_transcript && (
@@ -198,9 +208,16 @@ export default function MediaPage() {
);
case MediaFileType.Image:
let sdMediaImage = ["/images/avatars/default-pfp.png"];
let sdMediaImage = [
{ url: "/images/avatars/default-pfp.png", token: "default" },
];
if (mediaFile.public_bucket_path) {
sdMediaImage = [bucketConfig.getGcsUrl(mediaFile.public_bucket_path)];
sdMediaImage = [
{
url: bucketConfig.getGcsUrl(mediaFile.public_bucket_path),
token: mediaFile?.token,
},
];
}
return (
@@ -208,6 +225,7 @@ export default function MediaPage() {
<SdBatchMediaPanel
key={images.length}
images={mediaFile.maybe_batch_token ? images : sdMediaImage}
onActiveSlideChange={handleActiveSlideChange}
/>
{promptSection}
</>
@@ -285,9 +303,12 @@ export default function MediaPage() {
? mediaTypeLabels[mediaFile?.media_type]
: "";
let audioLink = new BucketConfig().getGcsUrl(mediaFile?.public_bucket_path);
let downloadLink =
activeSlide.url || bucketConfig.getGcsUrl(mediaFile?.public_bucket_path);
const shareUrl = `https://fakeyou.com/media/${mediaFile?.token || ""}`;
const shareUrl = `https://fakeyou.com/media/${
activeSlide.token || mediaFile?.token || ""
}`;
const shareText = "Check out this media on FakeYou.com!";
if (status < 3)
@@ -592,8 +613,8 @@ export default function MediaPage() {
icon={faArrowDownToLine}
label="Download"
className="flex-grow-1"
href={audioLink}
download={audioLink}
href={downloadLink}
download={downloadLink}
variant="secondary"
target="_blank"
/>
@@ -604,8 +625,8 @@ export default function MediaPage() {
icon={faArrowDownToLine}
square={true}
variant="secondary"
href={audioLink}
download={audioLink}
href={downloadLink}
download={downloadLink}
tooltip="Download"
target="_blank"
/>
@@ -9,32 +9,52 @@ import { FreeMode, Navigation, Thumbs } from "swiper/modules";
import { Swiper as SwiperType } from "swiper/types";
import "./SdBatchMediaPanel.scss";
interface SdCoverImagePanelProps {
images: string[];
interface ImageWithToken {
url: string;
token: string;
}
export default function SdBatchMediaPanel({ images }: SdCoverImagePanelProps) {
interface SdBatchMediaPanelProps {
images: ImageWithToken[];
onActiveSlideChange: (image: ImageWithToken) => void;
}
export default function SdBatchMediaPanel({
images,
onActiveSlideChange,
}: SdBatchMediaPanelProps) {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
const [forceUpdateKey, setForceUpdateKey] = useState(0);
const [isPortrait, setIsPortrait] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const [isFirstLoad, setIsFirstLoad] = useState(true);
useEffect(() => {
images.forEach(imgSrc => {
const currentUrl = new URL(window.location.href);
const currentToken = currentUrl.pathname.split("/media/")[1].split("?")[0];
const initialSlideIndex = images.findIndex(
image => image.token === currentToken
);
if (initialSlideIndex !== -1) {
setInitialSlide(initialSlideIndex);
}
setIsFirstLoad(false);
}, [images]);
useEffect(() => {
images.forEach(image => {
const img = new Image();
img.onload = () => {
// If any image is portrait, update isPortrait to true
if (img.height > img.width && !isPortrait) {
setIsPortrait(true);
}
};
img.src = imgSrc;
img.src = image.url;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [images]);
}, [images, isPortrait]);
useEffect(() => {
if (images.length > 0) {
// Force re-render of the Swiper component by updating a key
setForceUpdateKey(prevKey => prevKey + 1);
}
}, [images]);
@@ -43,6 +63,20 @@ export default function SdBatchMediaPanel({ images }: SdCoverImagePanelProps) {
setThumbsSwiper(swiper);
};
const handleSlideChange = (swiper: SwiperType) => {
if (!isFirstLoad) {
// Only update URL if it's not the first load
const currentSlideIndex = swiper.realIndex;
const currentImage = images[currentSlideIndex];
const currentImageToken = images[currentSlideIndex].token;
const baseUrl = window.location.href.split("/media/")[0]; // Get the base URL before the token
const queryParams = window.location.search; // Get query parameters if any
const newUrl = `${baseUrl}/media/${currentImageToken}${queryParams}`; // Construct the new URL
window.history.replaceState(null, "", newUrl); // Replace the current history state
onActiveSlideChange(currentImage);
}
};
const secondSwiperClass = `secondSwiper ${
isPortrait ? "portrait" : "landscape"
}`;
@@ -58,29 +92,30 @@ export default function SdBatchMediaPanel({ images }: SdCoverImagePanelProps) {
modules={[FreeMode, Navigation, Thumbs]}
className={secondSwiperClass}
slidesPerView={1}
initialSlide={0}
initialSlide={initialSlide}
onSlideChange={handleSlideChange}
>
{images.map((imgSrc, index) => (
{images.map((image, index) => (
<SwiperSlide key={index}>
<img src={imgSrc} alt={`Slide ${index + 1}`} />
<img src={image.url} alt={`Slide ${index + 1}`} />
</SwiperSlide>
))}
</Swiper>
{images.length > 1 && (
<Swiper
onSwiper={handleSwiper}
loop={true}
loop={false}
spaceBetween={10}
slidesPerView={5}
freeMode={true}
watchSlidesProgress={true}
modules={[FreeMode, Navigation, Thumbs]}
className="firstSwiper"
initialSlide={0}
initialSlide={initialSlide}
>
{images.map((imgSrc, index) => (
{images.map((image, index) => (
<SwiperSlide key={index}>
<img src={imgSrc} alt={`Thumbnail ${index + 1}`} />
<img src={image.url} alt={`Thumbnail ${index + 1}`} />
</SwiperSlide>
))}
</Swiper>
@@ -1,12 +1,6 @@
import React, { useRef, useState } from "react";
import { NavLink, useParams, useHistory } from "react-router-dom";
import {
Button,
Container,
Panel,
SelectionBubbles,
TextArea,
} from "components/common";
import { Button, Container, Panel, TextArea } from "components/common";
import VideoQuickTrim, {
QuickTrimData,
} from "components/common/VideoQuickTrim";
@@ -15,7 +9,7 @@ import EnqueueVideoStyleTransfer from "@storyteller/components/src/api/video_sty
import { initialValues } from "./defaultValues";
import { mapRequest, VSTType } from "./helpers";
import LoadingSpinner from "components/common/LoadingSpinner";
// import SectionAdvanceOptions from "./sectionAdvanceOptions";
import SelectionBubblesV2 from "components/common/SelectionBubblesV2";
export default function PageVSTApp() {
const { jobToken } = useParams<{ jobToken: string }>();
@@ -29,15 +23,14 @@ export default function PageVSTApp() {
// fileToken: job?.maybe_result?.entity_token || "",
});
console.log(job);
console.log(job?.maybe_result);
console.log(job?.maybe_result?.entity_token);
// console.log(job);
// console.log(job?.maybe_result);
// console.log(job?.maybe_result?.entity_token);
const handleOnChange = (val: {
[key: string]: number | string | boolean | undefined;
}) => {
setVstValues(curr => ({ ...curr, ...val }));
console.log("VST Values", vstValues);
};
const handleGenerate = () => {
@@ -54,7 +47,7 @@ export default function PageVSTApp() {
EnqueueVideoStyleTransfer(request).then(res => {
if (res.success && res.inference_job_token) {
console.log("Job enqueued successfully", res.inference_job_token);
// console.log("Job enqueued successfully", res.inference_job_token);
history.push(`/studio-intro/result/${res.inference_job_token}`);
} else {
console.log("Failed to enqueue job", res);
@@ -80,19 +73,39 @@ export default function PageVSTApp() {
};
}
const styleMap: { [key: string]: string } = {
Anime: "weight_yqexh77ntqyawzgh9fzash798", // set SD Model Weight Tokens, currently hardcoded all to Anime style
Pixel: "weight_yqexh77ntqyawzgh9fzash798",
Painting: "weight_yqexh77ntqyawzgh9fzash798",
};
const styleOptions = [
{
label: "Anime",
imageUrl: "/images/dummy-image.jpg",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Pixel",
imageUrl: "/images/dummy-image-2.jpg",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Painting",
imageUrl: "/images/dummy-image-3.jpg",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
{
label: "Ink",
imageUrl: "/images/dummy-image-4.jpg",
token: "weight_yqexh77ntqyawzgh9fzash798",
},
];
const handleStyleSelection = (selected: string) => {
const selectedSdModelToken = styleMap[selected];
setVstValues(curr => ({ ...curr, sdModelToken: selectedSdModelToken }));
const handleStyleSelection = (selectedLabel: any) => {
const selectedOption = styleOptions.find(
option => option.label === selectedLabel
);
const selectedSdModelToken = selectedOption ? selectedOption.token : null;
if (selectedSdModelToken) {
setVstValues(curr => ({ ...curr, sdModelToken: selectedSdModelToken }));
}
};
const styleOptions = Object.keys(styleMap);
if (!jobToken) {
history.push("/");
}
@@ -130,10 +143,12 @@ export default function PageVSTApp() {
<div className="col-12 col-md-6 d-flex flex-column gap-3 justify-content-center">
<div>
<label className="sub-title">Select a Style</label>
<SelectionBubbles
options={styleOptions}
<SelectionBubblesV2
options={Object.values(styleOptions)}
onSelect={handleStyleSelection}
selectedStyle="outline"
variant="card"
mobileSideScroll={true}
/>
</div>
@@ -9,6 +9,7 @@ import {
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { faMessageImage } from "@fortawesome/pro-solid-svg-icons";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
//import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
interface TextToImagePageProps {
@@ -31,6 +32,8 @@ export default function TextToImagePage({
// return <StudioNotAvailable />;
//}
usePrefixedDocumentTitle("Text to Image");
return (
<Container type="panel">
<PageHeaderWithImage
@@ -1,7 +1,6 @@
import React, { useReducer, useEffect } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
import { useInferenceJobs, useLocalize } from "hooks";
import {
FrontendInferenceJobType,
@@ -13,6 +12,7 @@ import { states, reducer } from "./reducer";
import SubRoutes from "./routes";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { faFilms } from "@fortawesome/pro-solid-svg-icons";
import { VideoStyleTransferNotAvailable } from "v2/view/_common/VideoStyleTransferNotAvailable";
export default function VideoStyleTransfer(props: {
enqueueInferenceJob: (
@@ -48,8 +48,8 @@ export default function VideoStyleTransfer(props: {
}
}, [pageState, enqueueInferenceJob]);
if (!props.sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />;
if (!props.sessionWrapper.canAccessVideoStyleTransfer()) {
return <VideoStyleTransferNotAvailable />;
}
return (
@@ -11,9 +11,11 @@ export const inferenceFailures = (fail = "") => {
export function TableOfKeyValues(
{
title,
keyValues,
height
}:{
title: string;
keyValues:{
[key:string]:number|string|boolean|undefined
};
@@ -24,11 +26,11 @@ export function TableOfKeyValues(
height: height || "100%",
overflowY: "scroll",
overflowX: "clip",
border: "1px solid white",
borderTopLeftRadius: "1rem",
borderBottomLeftRadius: "1rem",
// border: "1px solid white",
// borderTopLeftRadius: "1rem",
// borderBottomLeftRadius: "1rem",
}}><tbody>
<tr><th>Debug of a certain Key-Values set</th></tr>
<tr><th>{title}</th></tr>
{
Object.entries(keyValues).map(([key, val], index: number)=>{
return <tr key={index}><td>{`${key}`}</td><td>{`${val}`}</td></tr>
@@ -0,0 +1,110 @@
import React, {
useState
} from "react";
import { useHistory } from "react-router-dom";
import {
Accordion,
Button,
TempTextArea as TextArea,
} from "components/common";
import { mapRequest, VSTType } from './helpers';
import { TableOfKeyValues } from "../../commons";
import { Action } from "../../reducer";
import EnqueueVideoStyleTransfer from "@storyteller/components/src/api/video_styleTransfer";
export const CompAdminPanel = ({
parentPath,
vstValues,
dispatchPageState
}:{
parentPath: string;
vstValues : VSTType;
dispatchPageState: (action: Action) => void;
})=>{
const history = useHistory();
const exampleJSON = JSON.stringify(mapRequest(vstValues), null, 4);
const [jsonRequest, setJsonRequest] = useState<string>(exampleJSON);
const handleSendRequest = ()=>{
EnqueueVideoStyleTransfer(JSON.parse(jsonRequest)).then(res => {
if (res.success && res.inference_job_token) {
dispatchPageState({
type: "enqueueJobSuccess",
payload: {
inferenceJobToken: res.inference_job_token,
},
});
} else {
alert("Error, Check Console Log");
console.log(res);
}
});
dispatchPageState({ type: "enqueueJob" });
history.push(`${parentPath}/jobs`);
}
return (
<Accordion className="mt-4">
<Accordion.Item title={"Admin Panel"}>
<div className="row p-3 g-3">
<div className="col-8">
<TextArea
{...{
label:"JSON Input",
rows: 10,
placeholder: "Paste JSON here and send request directly",
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setJsonRequest(e.target.value),
value: jsonRequest,
required: false,
}}
/>
</div>
<div className="col-4 pt-5">
<Button
label="Console Log JSON Reqest"
variant="secondary"
onClick={()=>{
console.log(JSON.parse(jsonRequest));
}}
/>
<br/>
<Button
label="Send JSON"
onClick={handleSendRequest}
/>
</div>
</div>
<div className="row p-3 g-3">
<div className="col-8">
<TableOfKeyValues
title="All values in the VST app state"
keyValues={vstValues}
/>
</div>
<div className="col-4">
<p className="me-3"><b>Log Debug Values: </b></p>
<br/>
<Button
label="Console Log State"
variant="secondary"
onClick={()=>{
console.log(vstValues);
}}
/>
<br/>
<Button
label="Console Log Request"
variant="secondary"
onClick={()=>{
const request = mapRequest(vstValues);
console.log(request);
}}
/>
</div>
</div>
</Accordion.Item>
</Accordion>
);
}
@@ -28,7 +28,7 @@ import {
defaultPreset,
} from "./dataCnPresets";
export default function SectionAdvanceOptions({
export default function CompAdvanceOptions({
debug,
t,
vstValues: vstVal,
@@ -53,7 +53,7 @@ export default function CompStyleModal({
setOnSelectTimeStamp(new Date());
}
};
console.log(data);
//console.log(data);
return (
<div key={key}
className="col-12 col-sm-6 col-xl-4 grid-item"
@@ -14,6 +14,7 @@ import { mapRequest, VSTType } from "./helpers";
import CompStyleModal from "./compStyleModal";
import CompAdvanceOptions from "./compAdvanceOptions";
import { CompAdminPanel } from "./compAdminPanel";
export default function PageVSTApp({
debug: debugProps,
@@ -137,25 +138,11 @@ export default function PageVSTApp({
/>
</div>
{debug &&
<div className="col-12 d-flex justify-content-end align-items-center">
<p className="me-3"><b>Debug Values: </b></p>
<Button
className="me-3"
label="Console Log State"
onClick={()=>{
console.log(vstValues);
}}
variant="secondary"
/>
<Button
label="Console Log Request"
onClick={()=>{
const request = mapRequest(vstValues);
console.log(request);
}}
variant="secondary"
/>
</div>
<CompAdminPanel
parentPath={parentPath}
dispatchPageState={dispatchPageState}
vstValues={vstValues}
/>
}
</div>
</Panel>
@@ -12,6 +12,7 @@ export default function TabContentLibrary({
pageState: State;
dispatchPageState: (action: Action) => void;
}){
console.log(`Tab Rerender, selected: ${pageState.mediaFileToken}`);
const handleProceed = ()=>{
dispatchPageState({type: "loadFile"})
}
@@ -35,12 +36,16 @@ export default function TabContentLibrary({
inputLabel="Select a Video"
onSelect={handleOnSelect}
/>
{pageState.mediaFileToken &&
<VideoFakeyou mediaToken={pageState.mediaFileToken} />
<br/>
{pageState.mediaFileToken &&
<VideoFakeyou
height={500}
mediaToken={pageState.mediaFileToken}
/>
}
</div>
{pageState.status === states.FILE_SELECTED &&
<div className="col-12 d-flex justify-content-center mt-5">
<div className="col-12 d-flex justify-content-center mt-4">
<NavLink to={`load/${pageState.mediaFileToken}`}>
<Button
label={t("button.proceed")}
@@ -48,6 +48,7 @@ export default function VideoWorkflow(props:{
}, [pageState, enqueueInferenceJob]);
if (!props.sessionWrapper.canAccessStudio()) {
//return <VideoStyleTransferNotAvailable />
return <StudioNotAvailable />
}
@@ -270,8 +270,6 @@ function SdInferencePanel({
internalSeed.current = generateRandomSeed();
}
console.log(sdToken);
const request = {
uuid_idempotency_token: uuidv4(),
maybe_sd_model_token: sdToken || null,
@@ -287,13 +285,9 @@ function SdInferencePanel({
maybe_batch_count: batchCount,
};
console.log("request", request);
const response = await EnqueueImageGen(request);
if (EnqueueImageGenIsSuccess(response)) {
console.log("enqueuing...");
if (response.inference_job_token) {
enqueueInferenceJob(
response.inference_job_token,
+49
View File
@@ -4484,6 +4484,7 @@ __metadata:
react-select: ^5.7.0
react-share: ^4.4.1
react-twitch-embed: ^3.0.1
react-typeform-embed: ^1.0.8
react-wavesurfer.js: ^0.0.8
sass: ^1.57.1
swiper: ^11.0.5
@@ -4591,10 +4592,13 @@ __metadata:
resolution: "@storyteller/root@workspace:."
dependencies:
"@react-spring/types": ^9.7.3
"@typeform/embed": ^4.8.1
"@typeform/embed-react": ^3.15.0
craco: ^0.0.3
cross-env: ^7.0.3
husky: ^8.0.3
lerna: ^4.0.0
local-storage: ^2.0.0
moment: ^2.29.4
prettier: ^3.1.0
pretty-quick: ^3.1.3
@@ -5033,6 +5037,32 @@ __metadata:
languageName: node
linkType: hard
"@typeform/embed-react@npm:^3.15.0":
version: 3.15.0
resolution: "@typeform/embed-react@npm:3.15.0"
dependencies:
"@typeform/embed": 4.8.1
fast-deep-equal: ^3.1.3
peerDependencies:
react: ">=16.8.0"
checksum: d5e5d4b1cdfc128de2ce6c17ba648adbe98bfecc5a2c296fbedc8fb32da0c302e813baa6ce3ecc3de9dc84f36ccfd42250a4693f846d84ce2574e6fedcaa86e5
languageName: node
linkType: hard
"@typeform/embed@npm:4.8.1, @typeform/embed@npm:^4.8.1":
version: 4.8.1
resolution: "@typeform/embed@npm:4.8.1"
checksum: 1e7ac3d0d95c1ea823160f1478a973f871550a4bab7de1501f557b1e18966ab5a740b6a913d9436b5ee59d0f25d707b66582a8c41dea195383cca3afb1955b23
languageName: node
linkType: hard
"@typeform/embed@npm:^1.28.2":
version: 1.38.0
resolution: "@typeform/embed@npm:1.38.0"
checksum: 41115134e5cee28f5e031181b4525c7885d23707699cf183921110262717c7544bfd101428267410b812914c235687783e373ce98e37bdc447d72f8177663597
languageName: node
linkType: hard
"@types/aria-query@npm:^4.2.0":
version: 4.2.2
resolution: "@types/aria-query@npm:4.2.2"
@@ -14247,6 +14277,13 @@ __metadata:
languageName: node
linkType: hard
"local-storage@npm:^2.0.0":
version: 2.0.0
resolution: "local-storage@npm:2.0.0"
checksum: 921f381cafc4c0cac263a87e9e11093adf0bfb13b3520107bb5065bbccf5e7d22eea81148a40bc10d9f44aba8a5afe0157183205714d0221e02e36cb8afaa70b
languageName: node
linkType: hard
"locate-path@npm:^2.0.0":
version: 2.0.0
resolution: "locate-path@npm:2.0.0"
@@ -18177,6 +18214,18 @@ __metadata:
languageName: node
linkType: hard
"react-typeform-embed@npm:^1.0.8":
version: 1.0.8
resolution: "react-typeform-embed@npm:1.0.8"
dependencies:
"@typeform/embed": ^1.28.2
peerDependencies:
react: ^17.0.2
react-dom: ^17.0.2
checksum: f47ee0077046e282a482ef1a9d754ff255be65174e05b58f4c51c873ce8d771d1af98c5829f8860f2a33b0f4ebabcc4557e0f1e3b14b0d8bf3b23f460f1df18a
languageName: node
linkType: hard
"react-wavesurfer.js@npm:^0.0.5":
version: 0.0.5
resolution: "react-wavesurfer.js@npm:0.0.5"