replaced MediaFile instances of BucketConfig with server provided media links

This commit is contained in:
Victor committed 2024-09-09 22:13:34 -07:00
1 parent ffdb90e3aa
commit a68776f487
45 files changed
+604 -1173

No files matched your search

@@ -1,11 +1,11 @@
import { FetchStatus } from "./_common/SharedFetchTypes";
import { MediaFileType, GetMediaByUser } from "./media_files";
import { MediaFile, GetMediaByUser } from "./media_files";
import { Weight, ListWeights, ListFeaturedWeights } from "./weights";
import { GetBookmarksByUser } from "./bookmarks/GetBookmarksByUser";
import { MediaFileClass } from "./enums/MediaFileClass";
import { BucketConfig } from "./BucketConfig";
export type { MediaFileType, Weight };
export type { MediaFile, Weight };
export {
BucketConfig,
FetchStatus,
@@ -10,10 +10,28 @@ import {
WeightCategory,
} from "@storyteller/components/src/api/_common/enums";
export interface MediaVideoPreviews {
animated: string;
animated_thumbnail_template: string;
still: string;
still_thumbnail_template: string;
}
// export type MediaThumbFn = (width: number) => string;
export interface ResponseMediaLinks {
cdn_url: string;
// path for image media thumbnails
maybe_thumbnail_template: string | null;
// previews for video media
maybe_video_previews: MediaVideoPreviews | null;
}
export interface MediaFile {
token: string;
media_type: MediaFileType;
media_class: MediaFileClass | null;
media_links: ResponseMediaLinks;
maybe_engine_category: EngineCategory | null;
maybe_animation_type: AnimationType | null;
maybe_media_subtype: MediaFileSubtype | null;
@@ -49,6 +67,15 @@ export interface MediaFile {
maybe_text_transcript?: string | undefined;
}
// export interface MediaFile extends ResponseMediaFile {
// frontendURLs: {
// imageThumb: MediaThumbFn;
// main: string;
// videoStill: MediaThumbFn;
// videoAnimated: MediaThumbFn;
// };
// }
export interface MediaFileModeratorFields {
maybe_style_transfer_source_media_file_token?: string;
}
@@ -0,0 +1,32 @@
import { ResponseMediaLinks } from "./GetMedia";
const MediaLinker = (baseUrl: string) => (width: number) =>
baseUrl.replace("{WIDTH}", width.toString());
export type MediaLinkFn = (width: number) => string;
export interface MediaLinkUtility {
mainURL: string;
fixedVideoStill: string;
fixedVideoAnimated: string;
imageThumb: MediaLinkFn | null;
videoStill: MediaLinkFn | null;
videoAnimated: MediaLinkFn | null;
}
export function MediaLinks(mediaLinks?: ResponseMediaLinks): MediaLinkUtility {
return {
mainURL: mediaLinks?.cdn_url || "",
fixedVideoStill: mediaLinks?.maybe_video_previews?.still || "",
fixedVideoAnimated: mediaLinks?.maybe_video_previews?.animated || "",
imageThumb: mediaLinks?.maybe_thumbnail_template
? MediaLinker(mediaLinks.maybe_thumbnail_template)
: null,
videoStill: mediaLinks?.maybe_video_previews
? MediaLinker(mediaLinks.maybe_video_previews.still_thumbnail_template)
: null,
videoAnimated: mediaLinks?.maybe_video_previews
? MediaLinker(mediaLinks.maybe_video_previews.animated_thumbnail_template)
: null,
};
}
@@ -1,10 +1,11 @@
import { MediaFile } from "./GetMediaFile";
import { GetMediaByUser } from "./GetMediaByUser";
import {
GetMedia,
GetMediaRequest,
GetMediaResponse,
MediaFile,
} from "./GetMedia";
import { GetMediaByUser } from "./GetMediaByUser";
import { MediaLinks, MediaLinkUtility } from "./MediaLinks";
export type{
MediaFile as MediaFileType
}
export {
GetMediaByUser
}
export { GetMedia, GetMediaByUser, MediaLinks };
export type { GetMediaRequest, GetMediaResponse, MediaFile, MediaLinkUtility };
@@ -1,32 +1,28 @@
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
import { MediaFile as MediaFileOne } from "@storyteller/components/src/api/media_files/GetMedia";
import { MediaFile as MediaFileTwo } from "@storyteller/components/src/api/media_files/GetMediaFile";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
// Support multiple "Media File" API response payloads.
type MediaFile = MediaFileOne | MediaFileTwo ;
export function GetMediaFileTitle(mediaFile?: MediaFile) : string {
export function GetMediaFileTitle(mediaFile?: MediaFile): string {
if (mediaFile === undefined) {
return "Untitled file";
}
let mediaType = undefined;
switch (mediaFile?.media_type) {
case MediaFileType.Audio:
case MediaFileType.Video:
case MediaFileType.Image:
mediaType = mediaFile?.media_type.toLocaleLowerCase();
break;
mediaType = mediaFile?.media_type.toLocaleLowerCase();
break;
case MediaFileType.BVH:
case MediaFileType.GLTF:
case MediaFileType.GLB:
case MediaFileType.FBX:
mediaType = mediaFile?.media_type.toUpperCase();
break;
mediaType = mediaFile?.media_type.toUpperCase();
break;
case MediaFileType.SceneRon:
mediaType = "RON";
break;
mediaType = "RON";
break;
}
let title = "";
@@ -44,7 +40,7 @@ export function GetMediaFileTitle(mediaFile?: MediaFile) : string {
}
if (title.length < 4) {
const maybeSuffix = (!!mediaType) ? ` (${mediaType} file)` : "";
const maybeSuffix = !!mediaType ? ` (${mediaType} file)` : "";
return `${title}${maybeSuffix}`;
} else {
return title;
@@ -1,6 +1,5 @@
import React, { useEffect, useState, useRef } from "react";
import WaveSurfer from "wavesurfer.js";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { faPlay, faPause } from "@fortawesome/pro-solid-svg-icons";
import Button from "components/common/Button";
import "./AudioPlayer.scss";
@@ -20,8 +19,6 @@ export default function AudioPlayer({ src, id }: AudioPlayerProps) {
const isPlayerOnScreen = useOnScreen(waveformRef); //This stops the audio from playing when the player is not on screen
useEffect(() => {
const audioLink = new BucketConfig().getGcsUrl(src);
if (!waveSurferRef.current && waveformRef.current) {
const wavesurferInstance = WaveSurfer.create({
container: waveformRef.current,
@@ -36,7 +33,7 @@ export default function AudioPlayer({ src, id }: AudioPlayerProps) {
waveSurferRef.current = wavesurferInstance;
waveSurferRef.current.load(audioLink);
waveSurferRef.current.load(src);
waveSurferRef.current.on("pause", () => {
setIsPlaying(false);
@@ -27,7 +27,7 @@ export default function CameraInput({
const { open } = useModal();
const camera = useCameraState(true);
const [token, tokenSet] = useState("");
const { bucketUrl } = useMedia({ mediaToken: token });
const { links } = useMedia({ mediaToken: token });
const cameraSupported = MediaRecorder.isTypeSupported("video/mp4");
const cameraClick = () =>
@@ -83,7 +83,7 @@ export default function CameraInput({
<video
controls
{...{
src: bucketUrl,
src: links.mainURL,
}}
/>
<Button
@@ -7,11 +7,13 @@ import { CardFooter } from "components/entities";
import Badge from "components/common/Badge";
import { faThumbsUp, faWaveformLines } from "@fortawesome/pro-solid-svg-icons";
import useWeightTypeInfo from "hooks/useWeightTypeInfo/useWeightTypeInfo";
import WeightCoverImage from "components/common/WeightCoverImage";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import getCardUrl from "../getCardUrl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Stat from "components/common/Stat/Stat";
import {
// MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
// import getCardUrl from "../getCardUrl";
interface AudioCardProps {
@@ -41,6 +43,7 @@ export default function AudioCard({
onResultSelect,
onResultBookmarkSelect,
}: AudioCardProps) {
const { mainURL } = MediaLinks(data.media_links);
const linkUrl = getCardUrl(data, source, type);
const handleSelectModalResultSelect = () => {
@@ -62,28 +65,6 @@ export default function AudioCard({
data.weight_type || data.details?.maybe_weight_data?.weight_type
);
const bucketConfig = new BucketConfig();
let coverImage = undefined;
if (type === "media") {
coverImage = bucketConfig.getCdnUrl(data.public_bucket_path, 400, 100);
} else if (type === "weights") {
if (data?.cover_image?.maybe_cover_image_public_bucket_path) {
coverImage = bucketConfig.getCdnUrl(
data.cover_image.maybe_cover_image_public_bucket_path,
110,
100
);
}
if (data.details?.maybe_weight_data?.maybe_cover_image_public_bucket_path) {
coverImage = bucketConfig.getCdnUrl(
data.details?.maybe_weight_data?.maybe_cover_image_public_bucket_path,
110,
100
);
}
}
const card = (
<Card
padding={true}
@@ -113,13 +94,7 @@ export default function AudioCard({
</p>
)}
</div>
<AudioPlayer
src={
data.details?.maybe_media_file_data?.public_bucket_path ||
data.public_bucket_path
}
id={data.token}
/>
<AudioPlayer src={mainURL} id={data.token} />
<CardFooter
{...{
creator:
@@ -138,15 +113,6 @@ export default function AudioCard({
{type === "weights" && (
<>
<div className="d-flex">
{showCover && (
<WeightCoverImage
src={coverImage}
height={100}
width={100}
coverIndex={data?.cover_image?.default_cover?.image_index}
/>
)}
<div className="flex-grow-1">
<div className="d-flex align-items-center">
<div className="flex-grow-1">
@@ -8,6 +8,10 @@ import Button from "components/common/Button";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import useWeightTypeInfo from "hooks/useWeightTypeInfo/useWeightTypeInfo";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import {
// MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
// import useToken from "hooks/useToken";
import getCardUrl from "../getCardUrl";
@@ -38,6 +42,8 @@ export default function ImageCard({
// const { setToken, setWeightTitle } = useToken();
const linkUrl = getCardUrl(data, source, type);
const { imageThumb } = MediaLinks(data.media_links);
const handleSelectModalResultSelect = () => {
console.log("handleSelectModalResultSelect");
if (inSelectModal) {
@@ -63,10 +69,10 @@ export default function ImageCard({
);
const bucketConfig = new BucketConfig();
let coverImage = undefined;
let coverImage = "";
if (type === "media") {
coverImage = bucketConfig.getCdnUrl(data.details?.maybe_media_file_data?.public_bucket_path || data.public_bucket_path, 600, 100);
if (imageThumb && type === "media") {
coverImage = imageThumb(600);
} else if (type === "weights") {
coverImage = `/images/default-covers/${
data?.cover_image?.default_cover.image_index || 0
@@ -121,7 +127,9 @@ export default function ImageCard({
</div>
<CardFooter
{...{
creator: data?.maybe_creator || data.details?.maybe_media_file_data?.maybe_creator,
creator:
data?.maybe_creator ||
data.details?.maybe_media_file_data?.maybe_creator,
entityToken: data.details?.entity_token || data.token,
entityType: "media_file",
makeBookmarksProps: bookmarks?.makeProps,
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React from "react";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import {
@@ -7,10 +7,14 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import Badge from "components/common/Badge";
import Button from "components/common/Button";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { useHover } from "hooks";
import { Link } from "react-router-dom";
import getCardUrl from "../getCardUrl";
import { STYLES_BY_KEY } from "common/StyleOptions";
import {
// MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
interface VideoCardProps {
bookmarks?: any;
@@ -24,16 +28,15 @@ interface VideoCardProps {
}
export default function VideoCard({
bookmarks,
data,
ratings,
showCreator,
source = "",
type,
inSelectModal = false,
onResultSelect,
}: VideoCardProps) {
const linkUrl = getCardUrl(data, source, type);
const { videoAnimated, videoStill } = MediaLinks(data.media_links);
const [hover, hoverProps] = useHover({});
const handleSelectModalResultSelect = () => {
if (inSelectModal && onResultSelect) {
@@ -42,16 +45,7 @@ export default function VideoCard({
};
const timeAgo = useTimeAgo(data.created_at);
const [isHovered, setIsHovered] = useState(false);
const [imageSrc, setImageSrc] = useState("");
const [gifExists, setGifExists] = useState(false);
const [staticImageExists, setStaticImageExists] = useState(false);
const publicBucketPath = data.details
? data.details.maybe_media_file_data.public_bucket_path
: data.public_bucket_path;
const bucketConfig = new BucketConfig();
//video doesnt have random cover image endpoint or thumbnails yet
const defaultImageUrl = `/images/default-covers/${
data?.cover_image?.default_cover.image_index || 0
@@ -59,91 +53,8 @@ export default function VideoCard({
// We are checking the existence of the bucket gif files because it seems as though we can't check the cdn file's existence without running into cors issues
// CDN URLS
const staticImageUrl = publicBucketPath
? bucketConfig.getCdnUrl(publicBucketPath + "-thumb.jpg", 600, 100)
: defaultImageUrl;
const gifUrl = publicBucketPath
? bucketConfig.getCdnUrl(publicBucketPath + "-thumb.gif", 360, 20)
: null;
// BUCKET URLS
const bucketGifUrl = publicBucketPath
? bucketConfig.getGcsUrl(publicBucketPath + "-thumb.gif")
: null;
const bucketImageUrl = publicBucketPath
? bucketConfig.getGcsUrl(publicBucketPath + "-thumb.jpg")
: null;
// const bucketUrl = publicBucketPath
// ? new BucketConfig().getGcsUrl(publicBucketPath)
// : "";
const checkGifExists = async (url: string) => {
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok;
} catch (error) {
return false;
}
};
const checkImageExists = async (url: string) => {
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok;
} catch (error) {
return false;
}
};
// Preload images and check if the GIF exists when the component mounts
useEffect(() => {
// Check if the static image exists
if (bucketImageUrl === null) return;
checkImageExists(bucketImageUrl).then(staticExists => {
setStaticImageExists(staticExists);
if (staticExists) {
setImageSrc(bucketImageUrl);
} else {
setImageSrc(defaultImageUrl);
}
});
// Check and preload the GIF if it exists
if (bucketGifUrl === null) return;
checkGifExists(bucketGifUrl).then(gifExists => {
setGifExists(gifExists);
if (gifExists) {
const imgGif = new Image();
imgGif.src = bucketGifUrl;
}
});
}, [bucketGifUrl, staticImageUrl, defaultImageUrl, bucketImageUrl, data]);
useEffect(() => {
if (isHovered && gifExists && bucketGifUrl && staticImageExists) {
setImageSrc(gifUrl!);
} else if (staticImageExists) {
setImageSrc(bucketImageUrl!);
} else {
setImageSrc(defaultImageUrl);
}
}, [
isHovered,
gifExists,
gifUrl,
bucketGifUrl,
staticImageUrl,
staticImageExists,
defaultImageUrl,
bucketImageUrl,
data,
]);
useEffect(() => {
setImageSrc(defaultImageUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const staticImageUrl = videoStill && videoStill(600);
const gifUrl = videoAnimated && videoAnimated(360);
const styleLabel = STYLES_BY_KEY.has(data.maybe_style_name)
? STYLES_BY_KEY.get(data.maybe_style_name)?.label
@@ -169,16 +80,15 @@ export default function VideoCard({
const card = (
<Card
{...{ ...hoverProps }}
padding={false}
canHover={true}
onClick={handleSelectModalResultSelect}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{type === "media" && (
<>
<img
src={imageSrc}
src={gifUrl && hover ? gifUrl : staticImageUrl || defaultImageUrl}
alt={data.weight_name}
className="card-video"
loading="lazy"
@@ -1,109 +0,0 @@
@import "scss/colours.scss";
@import "scss/site-refresh.scss";
.fy-slide-frame {
position: absolute;
height: 100%;
width: 100%;
}
.fy-mocap-input {
@include panel;
border-radius: .5rem;
overflow: hidden;
position: relative;
// background-color: $panel;
}
.fy-media-input-square {
aspect-ratio: 1/1;
height: 300px;
.fy-media-picker-empty-controls {}
}
.fy-media-input-landscape {
width: 100%;
aspect-ratio: 4/3;
max-height: 66vh;
}
.fy-media-picker-empty, .fy-media-picker-full {
// position: relative;
// overflow: hidden;
.fy-media-picker-icon {
position: absolute;
height: 80%;
opacity: 75%;
// background-color: blue;
margin: auto;
inset: 0;
path { fill: $panel-inner-border}
}
}
.fy-media-picker-label-icon {
margin-right: .5rem;
height: 1.25rem;
}
.fy-media-picker-mocap-preview {
position: absolute;
border-radius: .5rem;
border: 1px solid rgba(#fff,.2);
display: block;
vertical-align: bottom;
height: 100%;
width: 100%;
margin: 0;
}
.fy-media-picker-full-controls {
position: absolute;
background-color: rgba(#000, .5);
padding: 1rem;
// backdrop-filter: blur(2px);
overflow: hidden;
border-radius: .5rem .5rem 0 0;
width: 100%;
}
.fy-media-picker-empty-controls {
// background-color: red;
position: absolute;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding: 1rem;
gap: 1rem;
.fy-media-picker-row {
height: 100%;
}
.fy-media-picker-button {
background-color: rgba(#000,.15);
border-radius: .5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
transition: .25s all ease-in-out;
color: rgba(#fff, .66);
&:hover {
background-color: rgba(#000,.1);
color: rgba(#fff, 1);
}
label { }
}
}
@@ -1,155 +0,0 @@
import React, { useState } from "react";
import { useLocation } from "react-router-dom";
import { a, useTransition } from "@react-spring/web";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import Iframe from "react-iframe";
import { MediaBrowser } from "components/modals";
import { FileWrapper, Label, Spinner } from "components/common";
import { EntityType, MediaFilterProp } from "components/entities/EntityTypes";
import { useFile, useMedia, useModal, useSession } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { faFileArrowUp, faGrid, faPersonWalking } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
import { UploadMedia, UploadMediaResponse } from "@storyteller/components/src/api/media_files/UploadMedia";
import { v4 as uuidv4 } from "uuid";
import "./MocapInput.scss"
interface Props {
label?: string,
onChange?: any,
type?: MediaFilterProp,
aspectRatio?: "square" | "landscape" | "portrait"
}
interface SlideProps {
media?: MediaFile
};
interface EmptySlideProps extends SlideProps {
inputProps?: any,
onSelect: any,
open: any,
type: MediaFilterProp,
user: any
};
interface AniProps {
animating: boolean,
className: string,
isLeaving: boolean,
render: any,
style: any
}
const MediaBusy = () => {
return <Spinner/>;
};
const MocapInputFull = ({ media }: SlideProps) => {
const bucketConfig = new BucketConfig();
const bvhUrl = media?.public_bucket_path ? bucketConfig.getGcsUrl(media.public_bucket_path) : "";
return <>
<Iframe
{...{
url: `https://engine.fakeyou.com?mode=viewer&bvh=${bvhUrl}`,
className: "fy-media-picker-mocap-preview",
}}
/>
<div {...{ className: "fy-media-picker-full-controls" }}>
Your file
</div>
</>;
};
const MediaPickerEmpty = ({ media, onSelect, open, inputProps, type, user }: EmptySlideProps) => {
const browserClick = () => open({
component: MediaBrowser,
props: { entityType: EntityType.media, onSelect, type, username: user?.username || "" }
});
return <>
<Icon {...{ className: "fy-media-picker-icon", icon: faPersonWalking }}/>
<div {...{ className: "fy-media-picker-empty-controls" }}>
<FileWrapper {...{ containerClass: "fy-media-picker-row", panelClass: "fy-media-picker-button", noStyle: true, ...inputProps }}>
<>
<Icon {...{ className: "fy-media-picker-label-icon", icon: faFileArrowUp }}/>
Upload, click or drag here
</>
</FileWrapper>
<div {...{ className: "fy-media-picker-row fy-media-picker-button", onClick: browserClick }}>
<Icon {...{ className: "fy-media-picker-label-icon", icon: faGrid }}/>
Choose from your media
</div>
</div>
</>;
}
const AniMod = ({ animating, className, isLeaving, render: Render, style, ...rest }: AniProps) => <a.div {...{
className: `fy-slide-frame${ className ? " " + className : "" }`,
style
}}>
<Render {...{ ...rest, animating }} />
</a.div>;
export default function MocapInput({ aspectRatio = "square", label, onChange, type }: Props) {
const { search } = useLocation();
const presetToken = search ? new URLSearchParams(search).get("preset_token") : "";
const [mediaToken,mediaTokenSet] = useState(presetToken || "");
const { media, mediaSet } = useMedia({ mediaToken });
const { user } = useSession();
const { open } = useModal();
const { clear, inputProps } = useFile({
onChange: (inputFile: any) => {
if (inputFile) UploadMedia({
uuid_idempotency_token: uuidv4(),
file: inputFile,
source: "file",
})
.then((res: UploadMediaResponse) => {
if ("media_file_token" in res) {
clear();
mediaTokenSet(res.media_file_token);
}
});
else console.log("🥺 no file");
}
});
const onSelect = (data: MediaFile) => {
mediaSet(data)
onChange({ target: { name: "temp", value: data.token } });
}
const busy = false;
const index = busy ? 0 : media ? 1 : 2;
const [animating,animatingSet] = useState(false);
const transitions = useTransition(index, {
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)` },
onRest: () => animatingSet(false),
onStart: () => animatingSet(true)
});
return <>
<Label {...{ label }}/>
<div {...{ className: `fy-mocap-input panel-inner${ aspectRatio ? " fy-media-input-" + aspectRatio : "" }`, }}>
{
// media ? <MocapInputFull {...{ media }}/> : <MediaPickerEmpty {...{ inputProps, media, onSelect, open, user }}/>
transitions((style: any, i: number, state: any) => {
let isLeaving = state.phase === "leave";
let sharedProps = { animating, isLeaving, media, style };
return [
<AniMod {...{ render: MediaBusy, className: "fy-media-picker-busy", ...sharedProps }}/>,
<AniMod {...{ render: MocapInputFull, className: "fy-media-picker-full", ...sharedProps }}/>,
<AniMod {...{ render: MediaPickerEmpty, className: "fy-media-picker-empty", inputProps, onSelect, open, type, user, ...sharedProps }}/>
][i];
})
}
</div>
</>;
};
@@ -1 +0,0 @@
export { default } from "./MocapInput";
@@ -95,6 +95,11 @@ describe("media files", () => {
token: "MEDIA_FILE_TOKEN",
media_type: MediaFileType.GLB,
media_class: MediaFileClass.Unknown,
media_links: {
cdn_url: "",
maybe_thumbnail_template: null,
maybe_video_previews: null,
},
maybe_engine_category: null,
maybe_animation_type: null,
maybe_media_subtype: null,
@@ -1,9 +1,10 @@
import { MediaFile as MediaFileOne } from "@storyteller/components/src/api/media_files/GetMedia";
import { MediaFile as MediaFileTwo } from "@storyteller/components/src/api/media_files/GetMediaFile";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { EngineMode } from "./EngineMode";
import { MediaFileSubtype } from "@storyteller/components/src/api/enums/MediaFileSubtype";
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { MediaFileClass } from "@storyteller/components/src/api/enums/MediaFileClass";
// Storyteller Engine parameters
@@ -11,69 +12,69 @@ import { MediaFileClass } from "@storyteller/components/src/api/enums/MediaFileC
// https://www.notion.so/storytellerai/Studio-Iframe-Query-Params-a748a9929ec3404780c3884e7fb89bdb
interface GetEngineUrlArgs {
mode: EngineMode,
mode: EngineMode;
// override the URL for local development
overrideURL?: string,
overrideURL?: string;
// Polymorphic type of asset we're loading into the engine.
asset: LoadableAsset
asset: LoadableAsset;
// Optional skybox color (hex) or name
// - Predefined skyboxes, eg. `gum_trees_4k`, `kloofendal_28d_misty_4k`, `meadow_4k`, `promenade_de_vidy_4k`, `scythian_tombs_4k`
// - Hex colors, eg. `ff0000` or `1a1a27`.
skybox?: string,
skybox?: string;
}
// Support multiple "Media File" API response payloads.
type MediaFile = MediaFileOne | MediaFileTwo ;
// type MediaFile = MediaFileOne | MediaFileTwo ;
// Used to load raw built-in objects into the engine.
interface EngineObject {
objectId: string;
}
// Used to load media files that are known a priori to be
// Used to load media files that are known a priori to be
// Storyteller Engine scenes (scene.ron files).
interface StorytellerSceneMediaFileToken {
storytellerSceneMediaFileToken: string;
}
// Used for loading media tokens of non-".scn.ron" files.
// The extension must be provided to the engine, and the
// The extension must be provided to the engine, and the
// extension must include the leading period.
interface SceneImportTokenAndExtension {
// Media file token for the asset
sceneImportToken: string;
// Extension, eg. ".glb", ".gltf", ".bvh".
// Extension, eg. ".glb", ".gltf", ".bvh".
// The extension must have a leading period.
extension: string;
}
export type LoadableAsset = MediaFile | EngineObject | StorytellerSceneMediaFileToken | SceneImportTokenAndExtension;
export type LoadableAsset =
| MediaFile
| EngineObject
| StorytellerSceneMediaFileToken
| SceneImportTokenAndExtension;
const ENGINE_BASE_URL = "https://engine.fakeyou.com";
export function GetEngineUrl(args: GetEngineUrlArgs) : string {
export function GetEngineUrl(args: GetEngineUrlArgs): string {
let engineUrl = `${args.overrideURL || ENGINE_BASE_URL}/?mode=${args.mode}`;
if (args.skybox) {
engineUrl += `&skybox=${args.skybox}`;
}
if (isMediaFile(args.asset)) {
const bucketAssetUrl = new BucketConfig().getGcsUrl(args.asset.public_bucket_path);
const { mainURL } = MediaLinks(args.asset.media_links);
if (
args.asset.media_type === MediaFileType.GLB &&
args.asset.media_class === MediaFileClass.Scene &&
(
args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene ||
args.asset.maybe_media_subtype === MediaFileSubtype.SceneImport
)
(args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene ||
args.asset.maybe_media_subtype === MediaFileSubtype.SceneImport)
) {
// GLB Scenes
// NB: Not sure if `maybe_media_subtype` is required to be "storyteller_scene".
@@ -81,7 +82,7 @@ export function GetEngineUrl(args: GetEngineUrlArgs) : string {
const sceneUrlRef = `remote://${args.asset.token}.glb`;
engineUrl += `&sceneImport=${sceneUrlRef}`;
} else if (
args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene ||
args.asset.maybe_media_subtype === MediaFileSubtype.StorytellerScene ||
args.asset.media_type === MediaFileType.SceneRon
) {
// Storyteller Engine Scenes
@@ -90,27 +91,21 @@ export function GetEngineUrl(args: GetEngineUrlArgs) : string {
// The engine, does, however, need a `.scn.ron` file extension.
const sceneUrlRef = `remote://${args.asset.token}.scn.ron`;
engineUrl += `&scene=${sceneUrlRef}`;
} else if (args.asset.maybe_media_subtype === MediaFileSubtype.Mixamo) {
engineUrl += `&mixamo=${mainURL}`;
} else if (args.asset.maybe_media_subtype === MediaFileSubtype.MocapNet) {
engineUrl += `&bvh=${mainURL}`;
} else if (
args.asset.maybe_media_subtype === MediaFileSubtype.Mixamo
args.asset.media_type === MediaFileType.BVH &&
(args.asset.maybe_media_subtype === null ||
args.asset.maybe_media_subtype === undefined)
) {
engineUrl += `&mixamo=${bucketAssetUrl}`;
} else if (
args.asset.maybe_media_subtype === MediaFileSubtype.MocapNet
) {
engineUrl += `&bvh=${bucketAssetUrl}`;
} else if (
args.asset.media_type === MediaFileType.BVH && (
args.asset.maybe_media_subtype === null ||
args.asset.maybe_media_subtype === undefined
)
){
// NB: Older BVH files that do not specify a subtype are MocapNet,
// NB: Older BVH files that do not specify a subtype are MocapNet,
// which will take a bare "bvh" argument.
engineUrl += `&bvh=${bucketAssetUrl}`;
engineUrl += `&bvh=${mainURL}`;
} else {
engineUrl += `&sceneImport=${bucketAssetUrl}`;
engineUrl += `&sceneImport=${mainURL}`;
}
} else if (isStorytellerSceneMediaFileToken(args.asset)) {
// Storyteller Engine Scenes
// NB: Storyteller Engine makes the API call to load the scene.
@@ -118,11 +113,9 @@ export function GetEngineUrl(args: GetEngineUrlArgs) : string {
// The engine, does, however, need a `.scn.ron` file extension.
const sceneUrlRef = `remote://${args.asset.storytellerSceneMediaFileToken}.scn.ron`;
engineUrl += `&scene=${sceneUrlRef}`;
} else if (isSceneImportTokenAndExtension(args.asset)) {
const sceneUrlRef = `remote://${args.asset.sceneImportToken}${args.asset.extension}`;
engineUrl += `&sceneImport=${sceneUrlRef}`;
} else if (isEngineObject(args.asset)) {
// NB: This is an engine built-in, eg. `couch.gltf` or `sample-room.gltf`.
engineUrl += `&objectId=${args.asset.objectId}`;
@@ -132,22 +125,25 @@ export function GetEngineUrl(args: GetEngineUrlArgs) : string {
}
// Type guard for media files
const isMediaFile = (loadable: LoadableAsset) : loadable is MediaFile => {
const isMediaFile = (loadable: LoadableAsset): loadable is MediaFile => {
return "token" in loadable;
}
};
// Type guard for engine objects
const isEngineObject = (loadable: LoadableAsset) : loadable is EngineObject => {
const isEngineObject = (loadable: LoadableAsset): loadable is EngineObject => {
return "objectId" in loadable;
}
};
// Type guard for .scn.ron media tokens
const isStorytellerSceneMediaFileToken = (loadable: LoadableAsset) : loadable is StorytellerSceneMediaFileToken => {
const isStorytellerSceneMediaFileToken = (
loadable: LoadableAsset
): loadable is StorytellerSceneMediaFileToken => {
return "storytellerSceneMediaFileToken" in loadable;
}
};
// Type guard for loadable media tokens (not .scn.ron)
const isSceneImportTokenAndExtension = (loadable: LoadableAsset) : loadable is SceneImportTokenAndExtension => {
const isSceneImportTokenAndExtension = (
loadable: LoadableAsset
): loadable is SceneImportTokenAndExtension => {
return "sceneImportToken" in loadable;
}
};
@@ -4,7 +4,7 @@ import MasonryGrid from "../MasonryGrid/MasonryGrid";
import { useListContent, useSession } from "hooks";
import prepFilter from "resources/prepFilter";
import MediaCards from "../Card/MediaCards";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { GetMediaByUser } from "@storyteller/components/src/api/media_files/GetMediaByUser";
import SkeletonCard from "components/common/Card/SkeletonCard";
import Pagination from "components/common/Pagination";
@@ -1,45 +1,54 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from "react";
import WaveSurfer from "wavesurfer.js";
import { Button, TempSelect } from "components/common";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import { faPlay, faPause, faRepeat, faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import {
faPlay,
faPause,
faRepeat,
faArrowRight,
} from "@fortawesome/pro-solid-svg-icons";
interface Props {
actions?: any
actions?: any;
mediaFile?: MediaFile;
}
export default function TempAudioPlayer({ actions = [], mediaFile }: Props) {
const ref = useRef(null);
const [finished,finishedSet] = useState(false);
const [initialized,initializedSet] = useState(false);
const [playing,playingSet] = useState(false);
const [repeat,repeatSet] = useState(false);
const [speed,speedSet] = useState(1);
const [finished, finishedSet] = useState(false);
const [initialized, initializedSet] = useState(false);
const [playing, playingSet] = useState(false);
const [repeat, repeatSet] = useState(false);
const [speed, speedSet] = useState(1);
const waveSurferRef = useRef<any>({ playing: () => false });
const { mainURL } = MediaLinks(mediaFile?.media_links);
const options = [
{ label: "x.5", value: .5 },
{ label: "x.5", value: 0.5 },
{ label: "x1", value: 1 },
{ label: "x1.5", value: 1.5 }
{ label: "x1.5", value: 1.5 },
];
const baseActions = [
{
icon: playing ? faPause : faPlay ,
const baseActions = [
{
icon: playing ? faPause : faPlay,
onClick: () => {
playingSet(!waveSurferRef.current.isPlaying());
waveSurferRef.current.playPause();
},
square: true
},
{
icon: repeat ? faArrowRight : faRepeat,
onClick: () => { repeatSet(!repeat) },
square: true,
variant: "secondary"
},
{
icon: repeat ? faArrowRight : faRepeat,
onClick: () => {
repeatSet(!repeat);
},
square: true,
variant: "secondary",
},
{
options,
@@ -47,12 +56,11 @@ export default function TempAudioPlayer({ actions = [], mediaFile }: Props) {
waveSurferRef.current.setPlaybackRate(target.value);
speedSet(target.value);
},
value: speed
value: speed,
},
...actions
...actions,
];
useEffect(() => {
if (!initialized && ref.current && mediaFile) {
initializedSet(true);
@@ -66,23 +74,37 @@ export default function TempAudioPlayer({ actions = [], mediaFile }: Props) {
cursorWidth: 2,
normalize: true,
});
waveSurfer.load(new BucketConfig().getGcsUrl(mediaFile?.public_bucket_path));
waveSurfer.on('ready', () => { waveSurferRef.current = waveSurfer });
waveSurfer.on('finish', () => { finishedSet(true); });
waveSurfer.load(mainURL);
waveSurfer.on("ready", () => {
waveSurferRef.current = waveSurfer;
});
waveSurfer.on("finish", () => {
finishedSet(true);
});
}
if (finished) {
finishedSet(false);
if (!repeat) { playingSet(false); }
else { waveSurferRef.current.playPause(); }
if (!repeat) {
playingSet(false);
} else {
waveSurferRef.current.playPause();
}
}
}, [finished, initialized, mainURL, mediaFile, repeat]);
},[finished,initialized,mediaFile,repeat]);
return <div {...{ className: "fy-audio-player" }}>
return (
<div {...{ className: "fy-audio-player" }}>
<div {...{ ref }}></div>
<div className="d-flex justify-content-center gap-2 mt-3">
{ baseActions.map((action,key) => action.options ? <TempSelect { ...{ ...action, key } }/> : <Button { ...{ ...action, key } }/>) }
{baseActions.map((action, key) =>
action.options ? (
<TempSelect {...{ ...action, key }} />
) : (
<Button {...{ ...action, key }} />
)
)}
</div>
</div>;
};
</div>
);
}
@@ -29,7 +29,6 @@ import JobQueueTicker from "./JobQueueTicker";
import Label from "./Label";
import LikeButton from "./LikeButton";
import LoginBlock from "./LoginBlock";
import MocapInput from "./MocapInput";
import MasonryGrid from "./MasonryGrid/MasonryGrid";
import Modal, { ModalUtilities } from "./Modal";
import Notification from "./Notification";
@@ -106,7 +105,6 @@ export {
Label,
LikeButton,
LoginBlock,
MocapInput,
MasonryGrid,
Modal,
NonRouteTabs,
@@ -1,31 +1,59 @@
import React from "react";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome"; // for now
import { faPersonWalking } from "@fortawesome/pro-solid-svg-icons";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
export const ImagePreview = ({ data }: { data: any }) => {
const bucketConfig = new BucketConfig();
const coverImage = bucketConfig.getCdnUrl(data.public_bucket_path, 600, 100);
export const ImagePreview = ({ data }: { data: MediaFile }) => {
const { mainURL, imageThumb } = MediaLinks(data.media_links);
return <img {...{ alt: data.weight_name, className: "card-img", src: coverImage }} />;
return (
<img
{...{
alt: "",
className: "card-img",
src: imageThumb ? imageThumb(600) : mainURL || "",
}}
/>
);
};
export const VideoPreview = ({ data }: { data: any }) => {
export const VideoPreview = ({
data,
hover,
}: {
data: MediaFile;
hover: boolean;
}) => {
const { videoAnimated, videoStill } = MediaLinks(data.media_links);
const { default_cover } = data?.cover_image || {};
const { default_cover, maybe_cover_image_public_bucket_path } = data?.cover_image || {};
const bucketConfig = new BucketConfig();
//video doesnt have random cover image endpoint or thumbnails yet
let coverImage = `/images/default-covers/${ default_cover?.image_index || 0 }.webp`;
const coverImage = `/images/default-covers/${
default_cover?.image_index || 0
}.webp`;
if (maybe_cover_image_public_bucket_path) {
coverImage = bucketConfig.getCdnUrl(maybe_cover_image_public_bucket_path, 600, 100);
}
return <img {...{ alt: data.weight_name, className: "card-video", src: coverImage }} />;
return (
<img
{...{
alt: "",
className: "card-video",
src:
hover && videoAnimated
? videoAnimated(360)
: videoStill
? videoStill(600)
: coverImage,
}}
/>
);
};
export const MocapPreview = () => <Icon {...{ className: "card-img", icon: faPersonWalking }}/>;
export const MocapPreview = () => (
<Icon {...{ className: "card-img", icon: faPersonWalking }} />
);
// export const ImagePreview = previwImg("card-img");
// export const VideoPreview = previwImg("card-video");
// export const VideoPreview = previwImg("card-video");
@@ -4,12 +4,14 @@ import getCardUrl from "components/common/Card/getCardUrl";
import "./CardWrapper.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStar } from "@fortawesome/pro-solid-svg-icons";
import { useHover } from "hooks";
import { MediaFile } from "@storyteller/components/src/api/media_files";
interface Props {
canHover?: boolean;
card: React.ElementType;
data: any;
data: MediaFile;
onClick?: (e: any) => any;
padding?: boolean;
preview: React.ElementType;
@@ -29,14 +31,15 @@ export default function CardWrapper({
featured,
...rest
}: Props) {
const [hover, hoverProps] = useHover({});
const linkUrl = getCardUrl(data, source, type);
const cardProps = { data, source, type, ...rest };
const cardProps = { data, hover, source, type, ...rest };
const className = `card ${padding ? "p-3" : ""} ${
featured ? "card-featured" : ""
} ${onClick || canHover ? "card-clickable" : ""}`.trim();
return onClick ? (
<div {...{ className, onClick: () => onClick(data) }}>
<div {...{ className, onClick: () => onClick(data), ...hoverProps }}>
<Card {...cardProps} />
{featured && (
<div className="card-featured-badge">
@@ -46,7 +49,7 @@ export default function CardWrapper({
)}
</div>
) : (
<Link {...{ className, to: linkUrl }}>
<Link {...{ className, to: linkUrl, ...hoverProps }}>
<Card {...cardProps} />
</Link>
);
@@ -2,7 +2,10 @@ import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { a, useTransition } from "@react-spring/web";
import useMeasure from "react-use-measure";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import Iframe from "react-iframe";
import { Area, Point } from "react-easy-crop";
import {
@@ -20,7 +23,6 @@ import {
} from "components/entities/EntityTypes";
import { WorkIndicator } from "components/svg";
import { useMedia, useMediaUploader } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
import EntityInputEmpty from "./EntityInputEmpty";
import EntityInputSidePanel from "./EntityInputSidePanel";
@@ -112,15 +114,13 @@ const MediaBusy = ({ uploaderBusy, uploadProgress }: SlideProps) => {
};
const EntityInputFull = ({ clear, cropProps, isNarrow, media }: SlideProps) => {
const bucketConfig = new BucketConfig();
const mediaUrl = media?.public_bucket_path
? bucketConfig.getGcsUrl(media.public_bucket_path)
: "";
const mediaType = mediaCategoryfromString(media?.media_type || "");
const [crop, cropSet] = useState<Point>({ x: 0, y: 0 });
const [zoom, zoomSet] = useState(1);
const { mainURL } = MediaLinks(media?.media_links);
const zoomSliderChange = ({ target }: ZoomSliderOnChangeEvent) =>
zoomSet(target.value);
@@ -133,7 +133,7 @@ const EntityInputFull = ({ clear, cropProps, isNarrow, media }: SlideProps) => {
crop,
cropProps,
cropSet,
image: mediaUrl,
image: mainURL,
zoom,
zoomSet,
}}
@@ -159,7 +159,7 @@ const EntityInputFull = ({ clear, cropProps, isNarrow, media }: SlideProps) => {
crop,
cropProps,
cropSet,
video: mediaUrl,
video: mainURL,
zoom,
zoomSet,
}}
@@ -182,7 +182,7 @@ const EntityInputFull = ({ clear, cropProps, isNarrow, media }: SlideProps) => {
<>
<Iframe
{...{
url: `https://engine.fakeyou.com?mode=viewer&${media?.media_type}=${mediaUrl}`,
url: `https://engine.fakeyou.com?mode=viewer&${media?.media_type}=${mainURL}`,
className: "fy-entity-input-mocap-preview",
}}
/>
@@ -272,9 +272,9 @@ export default function EntityInput({
onError: () => {
// @ts-ignore
window.dataLayer.push({
"event": "upload_failure",
"page": GApage || "/",
"user_id": "$user_id"
event: "upload_failure",
page: GApage || "/",
user_id: "$user_id",
});
},
onSuccess: (res: UploaderResponse) => {
@@ -330,8 +330,9 @@ export default function EntityInput({
<Label {...{ label }} />
<div
{...{
className: `fy-entity-input ${className ? " " + className : ""}${isNarrow ? " fy-entity-input-narrow" : " fy-entity-input-wide"
}`,
className: `fy-entity-input ${className ? " " + className : ""}${
isNarrow ? " fy-entity-input-narrow" : " fy-entity-input-wide"
}`,
ref: outerRef,
}}
>
@@ -5,32 +5,50 @@ import CardBadge from "../CardBadge";
import CardFooter from "../CardFooter";
interface Props {
data: any,
makeRatingsProps?: MakeRatingsProps,
preview: React.ElementType,
showCreator?: boolean
data: any;
hover: boolean;
makeRatingsProps?: MakeRatingsProps;
preview: React.ElementType;
showCreator?: boolean;
}
export default function OverlayCard({ data, makeRatingsProps, preview: Preview, showCreator }: Props) {
export default function OverlayCard({
data,
hover,
makeRatingsProps,
preview: Preview,
showCreator,
}: Props) {
const timeCreated = moment(data.created_at || "").fromNow();
return <>
<Preview {...{ data }}/>
<div {...{ className: "card-img-overlay" }}>
<div className="card-img-gradient" />
<CardBadge {...{ className: `fy-entity-type${ data.media_type ? "-" + data.media_type : "" }`, label: data.media_type }}/>
</div>
<div className="card-img-overlay-text">
<div>
<p className="fs-7 opacity-75 mb-0">{ timeCreated }</p>
return (
<>
<Preview {...{ data, hover }} />
<div {...{ className: "card-img-overlay" }}>
<div className="card-img-gradient" />
<CardBadge
{...{
className: `fy-entity-type${
data.media_type ? "-" + data.media_type : ""
}`,
label: data.media_type,
}}
/>
</div>
<CardFooter {...{
creator: data?.maybe_creator,
entityToken: data.token,
entityType: "media_file",
makeRatingsProps,
showCreator
}}/>
</div>
</>;
};
<div className="card-img-overlay-text">
<div>
<p className="fs-7 opacity-75 mb-0">{timeCreated}</p>
</div>
<CardFooter
{...{
creator: data?.maybe_creator,
entityToken: data.token,
entityType: "media_file",
makeRatingsProps,
showCreator,
}}
/>
</div>
</>
);
}
@@ -5,12 +5,15 @@ import {
FrontendInferenceJobType,
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import {
MediaFile,
MediaLinkUtility,
} from "@storyteller/components/src/api/media_files";
import {
CancelJob,
CancelJobResponse,
} from "@storyteller/components/src/api/jobs/CancelJob";
import { MediaURLs, useHover, useMedia, useSlides } from "hooks";
import { useHover, useMedia, useSlides } from "hooks";
import {
JobState,
jobStateCanChange,
@@ -33,35 +36,35 @@ interface JobListItem extends InferenceJob {
const BaseAction = ({
canStop = false,
hover = false,
links,
mediaFile,
maybeResultToken = "",
success = false,
toggleSlide = () => {},
urls,
}: {
canStop: boolean;
hover: boolean;
links: MediaLinkUtility;
mediaFile: MediaFile;
maybeResultToken: string;
success: boolean;
toggleSlide: () => void;
urls: MediaURLs;
}) =>
canStop || success ? (
<>
<JobResultPreview
{...{
hover,
links,
mediaFile,
mediaToken: maybeResultToken,
show: success,
urls,
}}
/>
{success && (
<Button
{...{
href: urls.file,
href: links.mainURL,
icon: faArrowDownToLine,
onClick: (e: any) => {
e.stopPropagation();
@@ -206,7 +209,7 @@ export default function JobItem({
}
};
const { mediaFile, urls } = useMedia({ mediaToken: maybeResultToken || "" });
const { links, mediaFile } = useMedia({ mediaToken: maybeResultToken || "" });
const slides = useSlides({
index,
@@ -216,11 +219,11 @@ export default function JobItem({
props: {
canStop: canStop(),
hover,
links,
maybeResultToken,
mediaFile,
success,
toggleSlide,
urls,
},
},
{ component: StopConfirm, props: { stopClick, toggleSlide } },
@@ -1,23 +1,24 @@
import React from "react";
import { MediaURLs } from "hooks";
// import { MediaFileClass } from "../enums/MediaFileClass";
import { MediaFileClass } from "@storyteller/components/src/api";
import { a, TransitionFn, useTransition } from "@react-spring/web";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import {
MediaFile,
MediaLinkUtility,
} from "@storyteller/components/src/api/media_files";
interface JobResultPreviewProps {
hover: boolean;
links: MediaLinkUtility;
mediaFile: MediaFile;
show?: boolean;
urls: MediaURLs;
}
export default function JobResultPreview({
hover,
links,
mediaFile,
show,
urls,
}: JobResultPreviewProps) {
const transitions: TransitionFn<boolean, { opacity: number }> = useTransition(
hover,
@@ -39,7 +40,7 @@ export default function JobResultPreview({
<a.img
{...{
className: "fy-inference-job-preview-static",
src: urls.thumb(),
src: links.videoStill ? links.videoStill(100) : "",
}}
/>
{transitions((style, isHovering) =>
@@ -47,7 +48,7 @@ export default function JobResultPreview({
<a.img
{...{
className: "fy-inference-job-preview-gif",
src: urls.gif(),
src: links.videoAnimated ? links.videoAnimated(100) : "",
style,
}}
/>
@@ -2,14 +2,14 @@ import { useEffect, useState } from "react";
import {
GetMedia,
MediaFile,
} from "@storyteller/components/src/api/media_files/GetMedia";
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import {
GetPrompts,
Prompt,
} 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";
type ThumbFn = (width?: number, quality?: number) => string;
@@ -50,19 +50,7 @@ export default function useMedia({
status === FetchStatus.in_progress ||
writeStatus === FetchStatus.in_progress;
const bucketConfig = new BucketConfig();
const publicBucketPath = media?.public_bucket_path || "";
const bucketUrl = bucketConfig.getGcsUrl(publicBucketPath);
const urls = {
file: bucketConfig.getGcsUrl(publicBucketPath),
gif: (width = 360, quality = 20) =>
bucketConfig.getCdnUrl(publicBucketPath + "-thumb.gif", width, quality),
thumb: (width = 600, quality = 100) =>
bucketConfig.getCdnUrl(publicBucketPath + "-thumb.jpg", width, quality),
};
const links = MediaLinks(media?.media_links);
useEffect(() => {
// this condidition handles all media file fetches
@@ -103,7 +91,8 @@ export default function useMedia({
return {
busy,
bucketUrl,
// bucketUrl,
links,
media,
mediaFile: media,
mediaSet,
@@ -112,7 +101,7 @@ export default function useMedia({
reload,
status,
statusSet,
urls,
// urls,
writeStatus,
};
}
@@ -1,7 +1,10 @@
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import React, { useEffect, useState } from "react";
import SceneCard from "./SceneCard";
import Marquee from "react-fast-marquee";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
interface ScrollingSceneCarouselProps {
small?: boolean;
@@ -11,15 +14,6 @@ interface ScrollingSceneCarouselProps {
className?: string;
}
interface MediaItem {
token: string;
public_bucket_path: string;
maybe_creator?: {
username: string;
};
maybe_title?: string;
}
export default function ScrollingSceneCarousel({
small,
showGradient = true,
@@ -27,8 +21,7 @@ export default function ScrollingSceneCarousel({
pauseOnHover = false,
className,
}: ScrollingSceneCarouselProps) {
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
const bucketConfig = new BucketConfig();
const [mediaItems, setMediaItems] = useState<MediaFile[]>([]);
useEffect(() => {
fetchMediaItems();
@@ -79,11 +72,11 @@ export default function ScrollingSceneCarousel({
{firstHalf.map((item, index) => (
<div key={index}>
<SceneCard
image={bucketConfig.getCdnUrl(
item.public_bucket_path + "-thumb.gif",
360,
20
)}
image={
MediaLinks(item.media_links).videoAnimated
? MediaLinks(item?.media_links).videoAnimated!(360)
: ""
}
alt={`Scene ${index}`}
title={item.maybe_title || "Scene"}
token={item.token}
@@ -105,11 +98,11 @@ export default function ScrollingSceneCarousel({
{secondHalf.map((item, index) => (
<div key={index}>
<SceneCard
image={bucketConfig.getCdnUrl(
item.public_bucket_path + "-thumb.gif",
360,
20
)}
image={
MediaLinks(item.media_links).videoAnimated
? MediaLinks(item?.media_links).videoAnimated!(360)
: ""
}
alt={`Scene ${index}`}
title={item.maybe_title || "Scene"}
token={item.token}
@@ -1,24 +1,19 @@
import React, { useEffect, useState } from "react";
import "./PrelaunchLanding.scss";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { MainButton } from "./MainButton";
interface MediaItem {
token: string;
public_bucket_path: string;
maybe_creator: {
username: string;
};
}
interface Props {
sessionWrapper: SessionWrapper;
}
function PrelaunchLanding(props: Props) {
const totalGrids = 63;
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
const [mediaItems, setMediaItems] = useState<MediaFile[]>([]);
const [blankGrids, setBlankGrids] = useState<number[]>([15, 16, 17, 22, 23]);
const [screenSize, setScreenSize] = useState("lg");
@@ -133,14 +128,8 @@ function PrelaunchLanding(props: Props) {
}
const mediaItem = mediaItems[i - 1];
const bucketConfig = new BucketConfig();
const gifUrl = mediaItem
? bucketConfig.getCdnUrl(
mediaItem.public_bucket_path + "-thumb.gif",
360,
20
)
: "";
const { videoAnimated } = MediaLinks(mediaItem.media_links);
const gifUrl = videoAnimated ? videoAnimated(360) : "";
gridItems.push(
<div
@@ -164,7 +153,9 @@ function PrelaunchLanding(props: Props) {
}
src={gifUrl}
className="w-100 h-100 object-fit-cover"
alt={`Video by ${mediaItem.maybe_creator.username}`}
alt={`Video by ${
mediaItem.maybe_creator_user?.username || "user"
}`}
/>
) : null}
</div>
@@ -31,7 +31,11 @@ import {
import { useDebounce, useInferenceJobs, useLocalize, useModal } from "hooks";
import LipsyncAudioPlayer from "./LipsyncAudioPlayer";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
import {
GetMedia,
GetMediaResponse,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { SessionTtsInferenceResultList } from "v2/view/_common/SessionTtsInferenceResultsList";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { useHistory } from "react-router-dom";
@@ -185,18 +189,20 @@ export const GenerateTts = ({
if (maybeResultToken) {
const fetchMedia = async () => {
try {
const response = await GetMedia(maybeResultToken, {});
const response: GetMediaResponse = await GetMedia(
maybeResultToken,
{}
);
if (
response &&
response.media_file &&
response.media_file.public_bucket_path &&
response.media_file.maybe_text_transcript
) {
const audioLink = new BucketConfig().getGcsUrl(
response.media_file.public_bucket_path
);
if (audioLink && currentAudioUrl === null) {
setCurrentAudioUrl(audioLink);
const { mainURL } = MediaLinks(response.media_file.media_links);
if (mainURL && currentAudioUrl === null) {
setCurrentAudioUrl(mainURL);
setTranscript(response.media_file.maybe_text_transcript || "");
if (onResultToken) {
onResultToken(response.media_file.token);
@@ -32,9 +32,12 @@ import { useInferenceJobs, useLocalize, useSession } from "hooks";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { AITools } from "components/marketing";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import LoadingSpinner from "components/common/LoadingSpinner";
import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
import {
GetMedia,
// MediaFile, please use this type where applicabale
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { useLocation } from "react-router-dom";
import { useDocumentTitle } from "@storyteller/components/src/hooks/UseDocumentTitle";
import { useHistory } from "react-router-dom";
@@ -238,7 +241,7 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
};
const selectedSourceMediaLink = selectedSourceMedia?.public_bucket_path
? new BucketConfig().getGcsUrl(selectedSourceMedia.public_bucket_path)
? MediaLinks(selectedSourceMedia.media_links).mainURL
: null;
const handleJobProgress = (progressPercentage: number | null) => {
@@ -266,12 +269,10 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
response.media_file &&
response.media_file.public_bucket_path
) {
const mediaLink = new BucketConfig().getGcsUrl(
response.media_file.public_bucket_path
);
const { mainURL } = MediaLinks(response.media_file.media_links);
if (jobToken === lastEnqueuedJobToken) {
setGeneratedVideoSrc(mediaLink);
setGeneratedVideoSrc(mainURL);
setIsGenerating(false);
setJobPercentage(null);
@@ -40,9 +40,11 @@ import {
InferenceJob,
} from "@storyteller/components/src/jobs/InferenceJob";
import { AITools } from "components/marketing";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import SessionLpInferenceResultsList from "./SessionLpInferenceResultsList";
import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
import {
GetMedia,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { useLocation } from "react-router-dom";
import { LivePortraitDetails } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
import { useDocumentTitle } from "@storyteller/components/src/hooks/UseDocumentTitle";
@@ -359,9 +361,7 @@ export default function LivePortrait() {
response.media_file &&
response.media_file.public_bucket_path
) {
const mediaLink = new BucketConfig().getGcsUrl(
response.media_file.public_bucket_path
);
const { mainURL } = MediaLinks(response.media_file.media_links);
const sourceIndex = sourceTokens.indexOf(
livePortraitDetails.source_media_file_token
@@ -375,7 +375,7 @@ export default function LivePortrait() {
motionIndex,
sourceToken: livePortraitDetails.source_media_file_token,
motionToken: livePortraitDetails.face_driver_media_file_token,
videoSrc: mediaLink,
videoSrc: mainURL,
jobToken,
createdAt,
};
@@ -392,7 +392,7 @@ export default function LivePortrait() {
selectedSourceIndex === newGeneratedVideo.sourceIndex &&
selectedMotionIndex === newGeneratedVideo.motionIndex
) {
setGeneratedVideoSrc(mediaLink);
setGeneratedVideoSrc(mainURL);
setIsGenerating(false);
}
@@ -40,10 +40,12 @@ import {
} from "@storyteller/components/src/jobs/InferenceJob";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { AITools } from "components/marketing";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import LoadingSpinner from "components/common/LoadingSpinner";
import SessionLpInferenceResultsList from "./SessionLpInferenceResultsList";
import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
import {
GetMedia,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { useLocation } from "react-router-dom";
import { LivePortraitDetails } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
import { useDocumentTitle } from "@storyteller/components/src/hooks/UseDocumentTitle";
@@ -543,9 +545,8 @@ export default function LivePortrait({
setSelectedSourceMedia(media);
};
const selectedSourceMediaLink = selectedSourceMedia?.public_bucket_path
? new BucketConfig().getGcsUrl(selectedSourceMedia.public_bucket_path)
: null;
const selectedSourceMediaLink =
MediaLinks(selectedSourceMedia.media_links).mainURL || null;
const handleJobTokens = async (
maybeResultToken: string,
@@ -569,9 +570,7 @@ export default function LivePortrait({
response.media_file &&
response.media_file.public_bucket_path
) {
const mediaLink = new BucketConfig().getGcsUrl(
response.media_file.public_bucket_path
);
const { mainURL } = MediaLinks(response.media_file.media_links);
const sourceIndex = sourceTokens.indexOf(
livePortraitDetails.source_media_file_token
@@ -585,7 +584,7 @@ export default function LivePortrait({
motionIndex,
sourceToken: livePortraitDetails.source_media_file_token,
motionToken: livePortraitDetails.face_driver_media_file_token,
videoSrc: mediaLink,
videoSrc: mainURL,
jobToken,
createdAt,
};
@@ -602,7 +601,7 @@ export default function LivePortrait({
selectedSourceIndex === newGeneratedVideo.sourceIndex &&
selectedMotionIndex === newGeneratedVideo.motionIndex
) {
setGeneratedVideoSrc(mediaLink);
setGeneratedVideoSrc(mainURL);
setIsGenerating(false);
}
@@ -2,12 +2,12 @@ import React, { useCallback, useEffect, useState } from "react";
import { Badge, Button } from "components/common";
import FaceCropper from "./FaceCropper";
import ThumbnailItem from "./ThumbnailItem";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import LoadingSpinner from "components/common/LoadingSpinner";
import {
GetMedia,
MediaFile,
} from "@storyteller/components/src/api/media_files/GetMedia";
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import {
faUpload,
faChevronLeft,
@@ -116,9 +116,7 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
}, [selectedIndex]);
const selectedMedia = mediaData[mediaTokens[selectedIndex]];
const mediaLink = selectedMedia?.public_bucket_path
? new BucketConfig().getGcsUrl(selectedMedia.public_bucket_path)
: null;
const { mainURL } = MediaLinks(selectedMedia.media_links);
useEffect(() => {
if (onSelectedMediaChange) {
@@ -188,9 +186,9 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
{cropper && cropArea ? (
<>
{mediaLink ? (
{mainURL ? (
<FaceCropper
videoSrc={mediaLink}
videoSrc={mainURL}
onCropComplete={onCropComplete}
showGrid={isCropping ? true : false}
zoomWithScroll={isCropping ? true : false}
@@ -213,11 +211,11 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
<LoadingSpinner />
) : (
<>
{mediaLink ? (
{mainURL ? (
selectedMedia?.media_type === "image" ? (
<img
key={selectedIndex}
src={mediaLink}
src={mainURL}
alt="Selected media"
/>
) : (
@@ -232,7 +230,7 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
preload="auto"
draggable="false"
>
<source src={mediaLink} type="video/mp4" />
<source src={mainURL} type="video/mp4" />
Your browser does not support the video tag.
</video>
)
@@ -262,9 +260,7 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
<div className="row g-2 order-2 order-lg-3 position-relative">
{paginatedMediaTokens.map((token, index) => {
const media = mediaData[token];
const mediaLink = media?.public_bucket_path
? new BucketConfig().getGcsUrl(media.public_bucket_path)
: null;
const { mainURL: itemMainUrl } = MediaLinks(media.media_links);
return (
<ThumbnailItem
@@ -272,7 +268,7 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
index={index + currentPage * itemsPerPage}
selectedIndex={selectedIndex}
handleThumbnailClick={handleThumbnailClick}
poster={mediaLink || ""}
poster={itemMainUrl || ""}
mediaType={media?.media_type}
/>
);
@@ -2,8 +2,10 @@ import React, { useState } from "react";
import { useMedia, useSession } from "hooks";
import { Button, Container, Panel } from "components/common";
import { EntityInput } from "components/entities";
import { EditCoverImage, EditCoverImageResponse } from "@storyteller/components/src/api/media_files/EditCoverImage";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import {
EditCoverImage,
EditCoverImageResponse,
} from "@storyteller/components/src/api/media_files/EditCoverImage";
import "./EditCoverImage.scss";
import { useHistory, useParams } from "react-router-dom";
@@ -12,63 +14,75 @@ export default function EditCoverImagePage() {
const { studioAccessCheck } = useSession();
const history = useHistory();
const [coverToken, coverTokenSet] = useState("");
const { media } = useMedia({ mediaToken: targetToken });
const bucketConfig = new BucketConfig();
const currentSrc = bucketConfig.getGcsUrl(media?.cover_image?.maybe_cover_image_public_bucket_path || "");
const {
media,
links: { mainURL },
} = useMedia({ mediaToken: targetToken });
const onClick = () => {
if (coverToken) {
EditCoverImage(targetToken,{
EditCoverImage(targetToken, {
cover_image_media_file_token: coverToken,
})
.then((res: EditCoverImageResponse) => {
}).then((res: EditCoverImageResponse) => {
history.push(`/media/${targetToken}`);
});
}
};
return studioAccessCheck(<Container {...{
className: "cover-image-page mt-5",
type: "panel"
}}>
return studioAccessCheck(
<Container
{...{
className: "cover-image-page mt-5",
type: "panel",
}}
>
<Panel {...{ padding: true }}>
<header className="d-flex gap-3 flex-wrap">
<h1 className="fw-semibold">
Edit Cover Image
</h1>
<Button {...{
disabled: !coverToken,
label: "Update cover image",
onClick
}}/>
<h1 className="fw-semibold">Edit Cover Image</h1>
<Button
{...{
disabled: !coverToken,
label: "Update cover image",
onClick,
}}
/>
</header>
<div {...{
className: "cover-image-panel"
}}>
<div
{...{
className: "cover-image-panel",
}}
>
<div>
Current cover image
{
media?.cover_image?.maybe_cover_image_public_bucket_path ?
<img {...{
alt: "cover art",
className: "cover-image-current",
src: currentSrc
}}/> :
{media?.cover_image?.maybe_cover_image_public_bucket_path ? (
<img
{...{
alt: "cover art",
className: "cover-image-current",
src: mainURL,
}}
/>
) : (
<div {...{ className: "cover-image-empty" }}>No cover image</div>
}
)}
</div>
<div>
<EntityInput {...{
<EntityInput
{...{
accept: ["image"],
aspectRatio: "square",
label: "Choose a cover image",
name: "coverToken",
value: coverToken,
onChange: ({ target }: { target: any }) => { coverTokenSet(target.value) },
type: "media"
}}/>
onChange: ({ target }: { target: any }) => {
coverTokenSet(target.value);
},
type: "media",
}}
/>
</div>
</div>
</Panel>
</Container>);
};
</Container>
);
}
@@ -1,17 +0,0 @@
import React from "react";
import MediaAudioPlayer from "./MediaAudioPlayer";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
interface MediaAudioComponentProps {
mediaFile: MediaFile;
}
export default function MediaAudioComponent({
mediaFile,
}: MediaAudioComponentProps) {
return (
<div className="w-100">
<MediaAudioPlayer mediaFile={mediaFile} />
</div>
);
}
@@ -1,13 +1,15 @@
import React, { useEffect, useState, memo } from "react";
import WaveSurfer from "wavesurfer.js";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import {
faPlay,
faPause,
faRepeat,
faArrowRight,
} from "@fortawesome/pro-solid-svg-icons";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import Button from "components/common/Button";
enum PlaybackSpeed {
@@ -42,14 +44,13 @@ const MediaAudioPlayer = memo(({ mediaFile }: MediaAudioPlayerProps) => {
}, []);
useEffect(() => {
const audioLink = new BucketConfig().getGcsUrl(
mediaFile.public_bucket_path
);
const { mainURL } = MediaLinks(mediaFile.media_links);
if (waveSurfer) {
waveSurfer.load(audioLink);
waveSurfer.load(mainURL);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [waveSurfer, mediaFile.public_bucket_path, BucketConfig]);
}, [waveSurfer, mediaFile.public_bucket_path]);
useEffect(() => {
if (waveSurfer) {
@@ -1,18 +0,0 @@
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import React from "react";
interface MediaImageComponentProps {
mediaFile: MediaFile;
}
export default function MediaImageComponent({
mediaFile,
}: MediaImageComponentProps) {
const imagePath = new BucketConfig().getGcsUrl(mediaFile.public_bucket_path);
return (
<div>
<img src={imagePath} alt="test" />
</div>
);
}
@@ -1,7 +1,10 @@
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import MediaAudioPlayer from "./MediaAudioPlayer";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import Container from "components/common/Container";
import Panel from "components/common/Panel";
import PageHeader from "components/layout/PageHeader";
@@ -58,6 +61,7 @@ export default function MediaPage({
animationType,
animationTypeChange,
bookmarkButtonProps,
bucketUrl,
canAccessStudio,
canBanUsers,
canEdit,
@@ -89,7 +93,7 @@ export default function MediaPage({
.then(response => {
if (response.success) {
const mediaItems = response.results.map(result => ({
url: bucketConfig.getGcsUrl(result.public_bucket_path),
url: MediaLinks(result.media_links).mainURL,
token: result.token,
}));
setImages(mediaItems);
@@ -184,7 +188,7 @@ export default function MediaPage({
if (mediaFile.public_bucket_path) {
sdMediaImage = [
{
url: bucketConfig.getGcsUrl(mediaFile.public_bucket_path),
url: bucketUrl,
token: mediaFile?.token,
},
];
@@ -266,8 +270,7 @@ export default function MediaPage({
? mediaTypeLabels[mediaFile?.media_type]
: "";
let downloadLink =
activeSlide.url || bucketConfig.getGcsUrl(mediaFile?.public_bucket_path);
let downloadLink = activeSlide.url || bucketUrl;
const sharePath = `/media/${activeSlide.token || mediaFile?.token || ""}`;
@@ -6,7 +6,7 @@ import {
EngineCategory,
MediaFileType,
} from "@storyteller/components/src/api/_common/enums";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { MediaFile } from "@storyteller/components/src/api/media_files";
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
import { CreateFeaturedItem } from "@storyteller/components/src/api/featured_items/CreateFeaturedItem";
import { DeleteFeaturedItem } from "@storyteller/components/src/api/featured_items/DeleteFeaturedItem";
@@ -69,7 +69,7 @@ export default function MediaPageSwitch() {
const [engineCategory, engineCategorySet] =
useState<EngineCategoryOptions>(null);
const { bucketUrl, mediaFile, prompt, remove, status } = useMedia({
const { links, mediaFile, prompt, remove, status } = useMedia({
mediaToken: urlToken,
onSuccess: (res: any) => {
isFeaturedSet(res.is_featured);
@@ -80,6 +80,7 @@ export default function MediaPageSwitch() {
bookmarks.gather({ res, key: "token" });
},
});
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const canEdit = canEditMediaFile(mediaFile?.maybe_creator_user?.user_token);
@@ -181,7 +182,7 @@ export default function MediaPageSwitch() {
const subViewProps: MediaSubViewProps = {
animationType,
animationTypeChange,
bucketUrl,
bucketUrl: links.mainURL,
bookmarkButtonProps,
canAccessStudio,
canBanUsers,
@@ -1,5 +1,7 @@
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import React from "react";
interface MediaVideoComponentProps {
@@ -9,13 +11,11 @@ interface MediaVideoComponentProps {
export default function MediaVideoComponent({
mediaFile,
}: MediaVideoComponentProps) {
let mediaLink = mediaFile
? new BucketConfig().getGcsUrl(mediaFile.public_bucket_path)
: "";
const { mainURL } = MediaLinks(mediaFile?.media_links);
return mediaFile && mediaFile.public_bucket_path ? (
<video className="rounded" controls width="100%" height="auto">
<source src={mediaLink} />
<source src={mainURL} />
Your browser does not support the video element.
</video>
) : null;
@@ -1,11 +1,13 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import {
MediaFile,
MediaLinks,
} from "@storyteller/components/src/api/media_files";
import { Button, Panel } from "components/common";
import { STYLES_BY_KEY } from "common/StyleOptions";
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
interface PromptViewerProps {
isModerator: boolean;
@@ -21,6 +23,8 @@ export default function PromptViewer({
const [copyPositiveButtonText, setCopyPositiveButtonText] = useState("Copy");
const [copyNegativeButtonText, setCopyNegativeButtonText] = useState("Copy");
const { mainURL } = MediaLinks(mediaFile?.media_links);
const copyToClipboard = async (
text: string,
copyTextSet: React.Dispatch<React.SetStateAction<string>>
@@ -45,14 +49,7 @@ export default function PromptViewer({
</div>
<div className="panel-inner p-2 rounded">
<p className="fs-7">
<a
href={new BucketConfig().getGcsUrl(
mediaFile?.public_bucket_path.replace(
".mp4",
".no_watermark.mp4"
)
)}
>
<a href={mainURL.replace(".mp4", ".no_watermark.mp4")}>
Download Without Watermark
</a>{" "}
(Staff Only)
@@ -1,5 +1,5 @@
import React from "react";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import Scene3D from "components/common/Scene3D/Scene3D";
import { EngineMode } from "components/common/Scene3D/EngineMode";
@@ -12,10 +12,10 @@ const SKYBOX = "333348"; // Looks good (lighter)
//const SKYBOX = "3f3f55"; // too light
export interface EngineMediaPanelArgs {
mediaFile: MediaFile,
mediaFile: MediaFile;
}
export function EngineMediaPanel({ mediaFile } : EngineMediaPanelArgs) {
export function EngineMediaPanel({ mediaFile }: EngineMediaPanelArgs) {
return (
<Scene3D
mode={EngineMode.Viewer}
@@ -23,6 +23,6 @@ export function EngineMediaPanel({ mediaFile } : EngineMediaPanelArgs) {
fullScreen={false}
className="fy-studio-frame"
asset={mediaFile}
/>
/>
);
}
@@ -1 +0,0 @@
export { default } from "./pageWorkflowControls";
@@ -1,282 +0,0 @@
import React, {useState, useRef} from "react";
import { useParams, useHistory } from "react-router-dom";
import { useMedia } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { EnqueueVideoWorkflow } from "@storyteller/components/src/api/video_workflow";
import { states, Action, State } from "../../videoWorkflowReducer";
import {
Accordion,
Button,
ErrorMessage,
InputSeed,
Panel,
SelectModal,
Spinner,
TextArea
} from "components/common";
import SelectModalWeightsTabs from "components/common/SelectModalWeightsTabs";
import SectionControlNets, {ControlNetsInitialValues as CnIvs} from "./sectionControlNets";
import SectionVideoSettings, {VideoSettingsInitialValues as VideoIvs} from "./sectionVideoSettings";
import SectionAdvanceOptions, {AdvanceOptionsInitialValues as AdvIvs} from "./sectionAdvanceOptions";
import {
isInputValid,
mapRequest,
WorkflowValuesType
} from "./helpers";
export default function PageFilterControls({
debug, t, pageState, dispatchPageState, parentPath
}: {
debug?: boolean;
t: Function;
pageState: State;
parentPath: string;
dispatchPageState: (action: Action) => void;
}) {
const { mediaToken } = useParams<any>();
const videoRef = useRef<HTMLVideoElement>(null);
useMedia({
mediaToken: pageState.mediaFileToken || mediaToken,
onSuccess: (res: any) => {
dispatchPageState({
type: 'loadFileSuccess',
payload: {
mediaFile: res,
mediaFileToken: pageState.mediaFileToken || mediaToken
}
});
},
});
const [workflowValues, setWorkflowValues] = useState<WorkflowValuesType>({
fileToken: pageState.mediaFileToken || mediaToken,
outputPath: "",
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: "",
sdModelToken: "",
sdModelTitle:"",
loraModelToken: "",
loraModelTitle: "",
posPrompt: "",
negPrompt: "",
...VideoIvs,
...AdvIvs,
...CnIvs
});
const handleOnChange = (val:{[key: string]: number|string|boolean|undefined}) => {
setWorkflowValues((curr)=>({...curr, ...val}));
}
const handleReset = ()=>{
setWorkflowValues((curr)=>({
fileToken: pageState.mediaFileToken || mediaToken,
outputPath: "vid2vid/SparseUpscaleInterp_00001.mp4",
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: curr.seed,
sdModelToken: "",
sdModelTitle: "",
loraModelToken: "",
loraModelTitle: "",
posPrompt: "",
negPrompt: "",
...VideoIvs,
width: curr.width,
height: curr.height,
maxFrames: curr.maxFrames,
...AdvIvs,
...CnIvs
}));
}
const history = useHistory();
const handleGenerate = ()=>{
if(debug) console.log(workflowValues)
if (isInputValid(workflowValues)){
const request = mapRequest(workflowValues);
console.log(request);
EnqueueVideoWorkflow(request).then(res => {
if (res.success && res.inference_job_token) {
dispatchPageState({
type: 'enqueueFilterSuccess',
payload: {
inferenceJobToken: res.inference_job_token
}
})
}else{
console.log(res);
}
})
dispatchPageState({type: 'enqueueFilter'})
history.push(`${parentPath}/jobs`);
}else{
alert("you must pick an sd weight and input a prompt");
}
}
if (pageState.mediaFile){
const mediaLink = new BucketConfig().getGcsUrl(pageState.mediaFile.public_bucket_path);
if (mediaLink)
return(
<>
<Panel>
<div className="row g-3 p-3">
<div className="col-6">
<div className="fy-basic-video">
<video controls ref={videoRef}
style={{
maxHeight: "500px",
objectFit: "contain"
}}>
<source src={mediaLink} type="video/mp4"/>
</video>
</div>
</div>
<div className="col-6">
<SectionVideoSettings
workflowValues={workflowValues}
onChange={handleOnChange}
videoElement={videoRef?.current}
/>
<div className="d-flex my-3 justify-content-end">
<Button
className="me-3"
label={t("button.previewWorkflow")}
tooltip="feature not available yet"
variant="primary"
disabled
/>
<Button
className="me-3"
label={t("button.resetToDefault")}
tooltip="Reset all variables to default values"
variant="primary"
onClick={handleReset}
/>
<Button
className="me-3"
label={t("button.enqueueWorkflow")}
onClick={handleGenerate}
variant="primary"
/>
</div>
</div>
</div>
</Panel>
<Accordion className="mt-4">
<Accordion.Item title={"Basics"} defaultOpen>
<div className="row g-3 p-3">
<SelectModalWeightsTabs
modalTitle="Select a Stable Diffusion Weight"
inputLabel="Select a Stable Diffusion Weight"
weightType="sd_1.5"
onSelect={({title,token})=>{
handleOnChange({
"sdModelTitle": title,
"sdModelToken": token,
});
}}
value={{
token:workflowValues.sdModelToken,
title:workflowValues.sdModelTitle,
}}
/>
</div>
<div className="row g-3 p-3">
<SelectModal
modalTitle="Select a LoRA Weight"
label="Additional LoRA Weight"
onSelect={({title,token})=>{
console.log(`calling from select modal result select ${token}`);
handleOnChange({
"loraModelTitle": title,
"loraModelToken": token
});
}}
tabs={[
{
label: "All LoRA Weights",
tabKey: "allLoraWeights",
typeFilter: "loRA",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedLoraWeights",
typeFilter: "loRA",
searcher: false,
type: "weights",
},
]}
/>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<TextArea
{...{
label: "Prompt",
placeholder: "Enter a prompt",
onChange: (e:React.ChangeEvent<HTMLTextAreaElement>)=>handleOnChange({"posPrompt": e.target.value}),
value: workflowValues.posPrompt,
required: false,
}}
/>
</div>
<div className="col-md-6">
<TextArea
{...{
label: "Negative Prompt",
placeholder: "Enter Negative Prompt",
onChange: (e:React.ChangeEvent<HTMLTextAreaElement>)=>handleOnChange({"negPrompt": e.target.value}),
value: workflowValues.negPrompt,
required: false,
}}
/>
</div>
</div>
<div className="row g-3 p-3">
<InputSeed label="Seed" onChange={
(val:string)=>handleOnChange({seed: val})
}/>
</div>
</Accordion.Item>
<Accordion.Item title="Advance" defaultOpen>
<SectionAdvanceOptions
workflowValues={workflowValues}
onChange={handleOnChange}
/>
</Accordion.Item>
<Accordion.Item title="Control Nets" defaultOpen>
<SectionControlNets
workflowValues={workflowValues}
onChange={handleOnChange}
/>
</Accordion.Item>
</Accordion>
<div className="row g-3 py-3">
<div className="col-12 d-flex justify-content-end">
<Button
label={t("button.enqueueWorkflow")}
onClick={handleGenerate}
variant="primary"
/>
</div>
</div>
</>
);
}else if (pageState.status <= states.FILE_LOADING){
return (
<Panel>
<p>Loading Files</p>
<Spinner />
</Panel>
);
}
return <ErrorMessage />;
}
@@ -1,6 +1,6 @@
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
export enum states{
export enum states {
NO_FILE,
FILE_STAGED,
FILE_SELECTED,
@@ -9,7 +9,7 @@ export enum states{
FILE_LOADING,
FILE_LOADED,
WORKFLOW_ENQUEUEING,
WORKFLOW_ENQUEUED
WORKFLOW_ENQUEUED,
}
export type State = {
@@ -17,71 +17,72 @@ export type State = {
mediaFileToken?: string;
mediaFile?: MediaFile;
inferenceJobToken?: string;
}
};
export type Action =
| {type: 'reset'}
| {type: 'stagedFile'}
| {type: 'clearedFile'}
| {type: 'selectedFile', payload: { mediaFileToken: string}}
| {type: 'uploadFile'}
| {type: 'uploadFileSuccess', payload:{ mediaFileToken: string}}
| {type: 'loadFile'}
| {type: 'loadFileSuccess', payload:{
mediaFileToken: string, mediaFile: MediaFile
}}
| {type: 'enqueueFilter'}
| {type: 'enqueueFilterSuccess', payload: {inferenceJobToken: string|undefined}}
export type Action =
| { type: "reset" }
| { type: "stagedFile" }
| { type: "clearedFile" }
| { type: "selectedFile"; payload: { mediaFileToken: string } }
| { type: "uploadFile" }
| { type: "uploadFileSuccess"; payload: { mediaFileToken: string } }
| { type: "loadFile" }
| {
type: "loadFileSuccess";
payload: {
mediaFileToken: string;
mediaFile: MediaFile;
};
}
| { type: "enqueueFilter" }
| {
type: "enqueueFilterSuccess";
payload: { inferenceJobToken: string | undefined };
};
export function reducer (state: State, action: Action): State {
switch(action.type){
case 'stagedFile':
return {...state, status: states.FILE_STAGED}
case 'clearedFile':
return {...state,
status: states.NO_FILE,
mediaFileToken: ""
}
case 'selectedFile':
export function reducer(state: State, action: Action): State {
switch (action.type) {
case "stagedFile":
return { ...state, status: states.FILE_STAGED };
case "clearedFile":
return { ...state, status: states.NO_FILE, mediaFileToken: "" };
case "selectedFile":
return {
...state,
status: states.FILE_SELECTED,
mediaFileToken: action.payload.mediaFileToken
}
case 'uploadFile':
return {...state,status: states.FILE_UPLOADING};
case 'uploadFileSuccess':
mediaFileToken: action.payload.mediaFileToken,
};
case "uploadFile":
return { ...state, status: states.FILE_UPLOADING };
case "uploadFileSuccess":
return {
...state,
status: states.FILE_UPLOADED,
mediaFileToken: action.payload.mediaFileToken
}
case 'loadFile':
return {...state, status: states.FILE_LOADING}
case 'loadFileSuccess':
mediaFileToken: action.payload.mediaFileToken,
};
case "loadFile":
return { ...state, status: states.FILE_LOADING };
case "loadFileSuccess":
console.log("LOAD FILE SUCCESS");
return {
...state,
status: states.FILE_LOADED,
mediaFile: action.payload.mediaFile,
mediaFileToken : action.payload.mediaFileToken,
}
case 'enqueueFilter':
mediaFileToken: action.payload.mediaFileToken,
};
case "enqueueFilter":
return {
...state,
status: states.WORKFLOW_ENQUEUEING
}
case 'enqueueFilterSuccess':
status: states.WORKFLOW_ENQUEUEING,
};
case "enqueueFilterSuccess":
return {
...state,
status: states.WORKFLOW_ENQUEUED,
inferenceJobToken: action.payload.inferenceJobToken
}
case 'reset':
inferenceJobToken: action.payload.inferenceJobToken,
};
case "reset":
default:
return {status: states.NO_FILE};
return { status: states.NO_FILE };
}
}
}
@@ -1,50 +1,60 @@
import React from "react";
import {Switch, Route, useRouteMatch, Redirect, useHistory} from "react-router-dom";
import PageWorkflowControls from "./components/pageWorkflowControls";
import {
Switch,
Route,
useRouteMatch,
Redirect,
useHistory,
} from "react-router-dom";
import PageVideoProvision from "./components/pageVideoProvision";
import PageWorkflowJoblist from "./components/pageWorkflowJoblist";
import {states,State,Action} from "./videoWorkflowReducer";
import { states, State, Action } from "./videoWorkflowReducer";
export default function SubRoutes ({
debug, t, pageState, dispatchPageState
}:{
debug?:boolean,
t:Function,
pageState:State,
dispatchPageState: (action: Action) => void
}){
export default function SubRoutes({
debug,
t,
pageState,
dispatchPageState,
}: {
debug?: boolean;
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const { path } = useRouteMatch();
const history = useHistory();
history.listen(({pathname}, action) => {
if ((pathname === path || pathname.match(/upload|select-media/g)) && pageState.status >= states.FILE_LOADING){
history.listen(({ pathname }, action) => {
if (
(pathname === path || pathname.match(/upload|select-media/g)) &&
pageState.status >= states.FILE_LOADING
) {
//reset page if landed on page "Freshly"
dispatchPageState({type: 'reset'})
dispatchPageState({ type: "reset" });
}
})
});
const commonPageProps = {
debug, parentPath: path, t, pageState, dispatchPageState
debug,
parentPath: path,
t,
pageState,
dispatchPageState,
};
return(
return (
<Switch>
<Route exact path={`${path}/load/:mediaToken`} >
<PageWorkflowControls {...commonPageProps}/>
</Route>
<Route exact path={`${path}/upload`}>
<PageVideoProvision {...commonPageProps}/>
<PageVideoProvision {...commonPageProps} />
</Route>
<Route exact path={`${path}/select-media`}>
<PageVideoProvision {...commonPageProps}/>
<PageVideoProvision {...commonPageProps} />
</Route>
<Route exact path={`${path}/jobs`}>
<PageWorkflowJoblist {...commonPageProps}/>
<PageWorkflowJoblist {...commonPageProps} />
</Route>
<Route path={`${path}`}>
<Redirect to={`${path}/upload`} />
</Route>
</Switch>
)
);
}