Merge branch 'feature/mvp' into bflat-fy-dynamic-storyteller-fakeyou

This commit is contained in:
BFlat authored and GitHub committed 2024-02-08 12:54:19 +07:00
commit 455a1ceda9
89 files changed
+1565 -881

No files matched your search

Regular → Executable
View File
File mode changed.
+2 -2
View File
@@ -496,8 +496,8 @@ class ApiConfig {
return `${this.getApiOrigin()}/v1/mocap/mocapnet/create`;
}
// =============== Storyteller Filter ===============
enqueueStorytellerFilter(): string {
// =============== Video Workflow ===============
enqueueVideoWorkflow(): string {
return `${this.getApiOrigin()}/v1/workflow/comfy/create`;
}
@@ -18,6 +18,9 @@ export interface UserInfo {
display_name: string,
email_gravatar_hash: string,
// Rollout feature flags
can_access_studio: boolean,
// Usage
can_use_tts: boolean,
can_use_w2l: boolean,
@@ -1,7 +0,0 @@
export type {
EnqueueStorytellerFilterRequest, EnqueueStorytellerFilterResponse,
} from './EnqueueStorytellerFilter';
export {
EnqueueStorytellerFilter
} from './EnqueueStorytellerFilter';
@@ -1,6 +1,6 @@
import { ApiConfig } from "@storyteller/components";
export interface EnqueueStorytellerFilterRequest {
export interface EnqueueVideoWorkflowRequest {
"uuid_idempotency_token": string,
"maybe_sd_model": string,
@@ -29,16 +29,16 @@ export interface EnqueueStorytellerFilterRequest {
}
}
export interface EnqueueStorytellerFilterResponse {
export interface EnqueueVideoWorkflowResponse {
success: boolean,
inference_job_token?: string,
}
export async function EnqueueStorytellerFilter(request: EnqueueStorytellerFilterRequest) : Promise<EnqueueStorytellerFilterResponse>
export async function EnqueueVideoWorkflow(request: EnqueueVideoWorkflowRequest) : Promise<EnqueueVideoWorkflowResponse>
{
// const endpoint = "/v1/workflow/comfy/create";
const endpoint = new ApiConfig().enqueueStorytellerFilter();
const endpoint = new ApiConfig().enqueueVideoWorkflow();
return await fetch(endpoint, {
method: 'POST',
@@ -0,0 +1,7 @@
export type {
EnqueueVideoWorkflowRequest, EnqueueVideoWorkflowResponse,
} from './EnqueueVideoWorkflow';
export {
EnqueueVideoWorkflow
} from './EnqueueVideoWorkflow';
+1 -1
View File
@@ -12,7 +12,7 @@ export enum FrontendInferenceJobType {
VideoMotionCapture,
ConvertFbxtoGltf,
EngineComposition,
StorytellerFilter,
VideoWorkflow,
}
// NB: Many of these fields are optional despite the response payload containing them
@@ -1,4 +1,5 @@
import { GetSessionInfo, GetSessionInfoIsOk, SessionInfoSuccessResponse } from '../api/session/GetSessionInfo'
import { StudioRolloutHostnameAllowed } from '../utils/StudioRolloutHostnameAllowed';
// A lot of the APIs return null or leave values absent.
// I need to pick a single strategy and stick with it.
@@ -44,6 +45,14 @@ export class SessionWrapper {
return this.sessionStateResponse?.logged_in || false;
}
// Feature flag rollout for Storyteller Studio
// This depends on the domain name and the backend.
public canAccessStudio() : boolean {
const hostnameAllowed = StudioRolloutHostnameAllowed();
const userAllowed = this.sessionStateResponse?.user?.can_access_studio || false;
return hostnameAllowed && userAllowed;
}
// Username is all lowercase
public getUsername() : string | undefined {
return this.sessionStateResponse?.user?.username;
@@ -0,0 +1,19 @@
// Check if hostname is allowed to display studio features.
// This should allow more frequently landing against production.
export function StudioRolloutHostnameAllowed() : boolean {
const allowed = StudioRolloutHostnameCheck();
return allowed;
}
function StudioRolloutHostnameCheck() : boolean {
// Do not allow "production" or "staging" domains to use studio yet.
// Other domains (local development and feature branches) are allowed.
switch (window.location.hostname) {
case 'fakeyou.com':
case 'storyteller.ai':
return false;
default:
return true;
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "ফেস অ্যানিমেটর",
"videoMotionCapture": "ভিডিও মোশন ক্যাপচার",
"videoTitle": "ভিডিও জেনারেশন",
"videoW2l": "ভিডিও লিপ সিঙ্ক"
"videoW2l": "ভিডিও লিপ সিঙ্ক",
"videoWorkflow": "ভিডিও ওয়ার্কফ্লো"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "ওয়ার্কফ্লো কন্ট্রোলে এগিয়ে যান",
"process": "কর্মপ্রবাহের প্রক্রিয়া",
"select": "নির্বাচন করুন",
"upload": "ভিডিও আপলোড কর"
},
"headings": {
"title": "ভিডিও ওয়ার্কফ্লো"
},
"tabTitle": {
"library": "মিডিয়া থেকে বেছে নিন",
"original": "আসল",
"preview": "পূর্বরূপ",
"upload": "আপলোড করুন"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Gesichtsanimator",
"videoMotionCapture": "Video-Bewegungserfassung",
"videoTitle": "Videogenerierung",
"videoW2l": "Video-Lippensynchronisation"
"videoW2l": "Video-Lippensynchronisation",
"videoWorkflow": "Video-Workflow"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Fahren Sie mit den Workflow-Kontrollen fort",
"process": "Vom Prozess zum Workflow",
"select": "Wählen",
"upload": "Video hochladen"
},
"headings": {
"title": "Video-Workflow"
},
"tabTitle": {
"library": "Wählen Sie aus Medien",
"original": "Original",
"preview": "Vorschau",
"upload": "Hochladen"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Face Animator",
"videoMotionCapture": "Video Motion Capture",
"videoTitle": "Video Generation",
"videoW2l": "Video Lip Sync"
"videoW2l": "Video Lip Sync",
"videoWorkflow": "Video Workflow"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Proceed to Workflow Controls",
"process": "Process to Workflow",
"select": "Select",
"upload": "Upload Video"
},
"headings": {
"title": "Video Workflow"
},
"tabTitle": {
"library": "Choose from media",
"original": "Original",
"preview": "Preview",
"upload": "Upload"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Animador de rostros",
"videoMotionCapture": "Captura de movimiento de vídeo",
"videoTitle": "Generación de vídeo",
"videoW2l": "Sincronización de labios de vídeo"
"videoW2l": "Sincronización de labios de vídeo",
"videoWorkflow": "Flujo de trabajo de vídeo"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Continúe con los controles del flujo de trabajo",
"process": "Proceso al flujo de trabajo",
"select": "Seleccionar",
"upload": "Subir vídeo"
},
"headings": {
"title": "Flujo de trabajo de vídeo"
},
"tabTitle": {
"library": "Elige entre los medios",
"original": "Original",
"preview": "Avance",
"upload": "Subir"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Animateur de visage",
"videoMotionCapture": "Capture de mouvement vidéo",
"videoTitle": "Génération vidéo",
"videoW2l": "Synchronisation labiale vidéo"
"videoW2l": "Synchronisation labiale vidéo",
"videoWorkflow": "Flux de travail vidéo"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Passer aux contrôles de flux de travail",
"process": "Du processus au flux de travail",
"select": "Sélectionner",
"upload": "Télécharger une video"
},
"headings": {
"title": "Flux de travail vidéo"
},
"tabTitle": {
"library": "Choisissez parmi les médias",
"original": "Original",
"preview": "Aperçu",
"upload": "Télécharger"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "फेस एनिमेटर",
"videoMotionCapture": "वीडियो मोशन कैप्चर",
"videoTitle": "वीडियो जनरेशन",
"videoW2l": "वीडियो लिप सिंक"
"videoW2l": "वीडियो लिप सिंक",
"videoWorkflow": "वीडियो वर्कफ़्लो"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "वर्कफ़्लो नियंत्रण के लिए आगे बढ़ें",
"process": "वर्कफ़्लो की प्रक्रिया",
"select": "चुनना",
"upload": "विडियो को अॅॅपलोड करें"
},
"headings": {
"title": "वीडियो वर्कफ़्लो"
},
"tabTitle": {
"library": "मीडिया से चुनें",
"original": "मूल",
"preview": "पूर्व दर्शन",
"upload": "डालना"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Animator Wajah",
"videoMotionCapture": "Pengambilan Gerakan Video",
"videoTitle": "Pembuatan Video",
"videoW2l": "Sinkronisasi Bibir Video"
"videoW2l": "Sinkronisasi Bibir Video",
"videoWorkflow": "Alur Kerja Video"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Lanjutkan ke Kontrol Alur Kerja",
"process": "Proses ke Alur Kerja",
"select": "Pilih",
"upload": "Mengunggah video"
},
"headings": {
"title": "Alur Kerja Video"
},
"tabTitle": {
"library": "Pilih dari media",
"original": "Asli",
"preview": "Pratinjau",
"upload": "Mengunggah"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Animatore di volti",
"videoMotionCapture": "Acquisizione del movimento video",
"videoTitle": "Generazione video",
"videoW2l": "Sincronizzazione labiale video"
"videoW2l": "Sincronizzazione labiale video",
"videoWorkflow": "Flusso di lavoro video"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Procedi ai controlli del flusso di lavoro",
"process": "Dal processo al flusso di lavoro",
"select": "Selezionare",
"upload": "Carica video"
},
"headings": {
"title": "Flusso di lavoro video"
},
"tabTitle": {
"library": "Scegli tra i media",
"original": "Originale",
"preview": "Anteprima",
"upload": "Caricamento"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "フェイスアニメーター",
"videoMotionCapture": "ビデオモーションキャプチャ",
"videoTitle": "ビデオ生成",
"videoW2l": "ビデオリップシンク"
"videoW2l": "ビデオリップシンク",
"videoWorkflow": "ビデオワークフロー"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "ワークフロー制御に進む",
"process": "プロセスからワークフローへ",
"select": "選択する",
"upload": "ビデオをアップロードする"
},
"headings": {
"title": "ビデオワークフロー"
},
"tabTitle": {
"library": "メディアから選ぶ",
"original": "オリジナル",
"preview": "プレビュー",
"upload": "アップロード"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "얼굴 애니메이터",
"videoMotionCapture": "비디오 모션 캡처",
"videoTitle": "비디오 생성",
"videoW2l": "비디오 립싱크"
"videoW2l": "비디오 립싱크",
"videoWorkflow": "비디오 워크플로우"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "워크플로 제어로 진행",
"process": "워크플로 프로세스",
"select": "선택하다",
"upload": "비디오 업로드"
},
"headings": {
"title": "비디오 워크플로우"
},
"tabTitle": {
"library": "미디어에서 선택",
"original": "원래의",
"preview": "시사",
"upload": "업로드"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Animador facial",
"videoMotionCapture": "Captura de movimento de vídeo",
"videoTitle": "Geração de Vídeo",
"videoW2l": "Sincronização labial de vídeo"
"videoW2l": "Sincronização labial de vídeo",
"videoWorkflow": "Fluxo de trabalho de vídeo"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Prossiga para os controles de fluxo de trabalho",
"process": "Processo para fluxo de trabalho",
"select": "Selecione",
"upload": "Envio vídeo"
},
"headings": {
"title": "Fluxo de trabalho de vídeo"
},
"tabTitle": {
"library": "Escolha entre mídias",
"original": "Original",
"preview": "Visualização",
"upload": "Carregar"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Лицевой Аниматор",
"videoMotionCapture": "Захват видео движения",
"videoTitle": "Создание видео",
"videoW2l": "Видео синхронизация губ"
"videoW2l": "Видео синхронизация губ",
"videoWorkflow": "Видео рабочий процесс"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Перейдите к элементам управления рабочим процессом",
"process": "От процесса к рабочему процессу",
"select": "Выбирать",
"upload": "Загрузить видео"
},
"headings": {
"title": "Видео рабочий процесс"
},
"tabTitle": {
"library": "Выбирайте из медиа",
"original": "Оригинал",
"preview": "Предварительный просмотр",
"upload": "Загрузить"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "แอนิเมชั่นใบหน้า",
"videoMotionCapture": "จับภาพวิดีโอเคลื่อนไหว",
"videoTitle": "การสร้างวิดีโอ",
"videoW2l": "ลิปซิงค์วิดีโอ"
"videoW2l": "ลิปซิงค์วิดีโอ",
"videoWorkflow": "เวิร์กโฟลว์วิดีโอ"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "ดำเนินการควบคุมเวิร์กโฟลว์",
"process": "กระบวนการสู่เวิร์กโฟลว์",
"select": "เลือก",
"upload": "อัปโหลดวิดีโอ"
},
"headings": {
"title": "เวิร์กโฟลว์วิดีโอ"
},
"tabTitle": {
"library": "เลือกจากสื่อ",
"original": "ต้นฉบับ",
"preview": "ดูตัวอย่าง",
"upload": "ที่อัพโหลด"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "Yüz Animatörü",
"videoMotionCapture": "Video Hareket Yakalama",
"videoTitle": "Video Oluşturma",
"videoW2l": "Video Dudak Senkronizasyonu"
"videoW2l": "Video Dudak Senkronizasyonu",
"videoWorkflow": "Video İş Akışı"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "İş Akışı Kontrollerine Devam Edin",
"process": "İş Akışına Geçiş Süreci",
"select": "Seçme",
"upload": "Video yükle"
},
"headings": {
"title": "Video İş Akışı"
},
"tabTitle": {
"library": "Medya arasından seçim yapın",
"original": "Orijinal",
"preview": "Ön izleme",
"upload": "Yüklemek"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "khuôn mặt hoạt hình",
"videoMotionCapture": "Quay video chuyển động",
"videoTitle": "Tạo video",
"videoW2l": "Hát nhép video"
"videoW2l": "Hát nhép video",
"videoWorkflow": "Quy trình làm việc video"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "Tiến tới Kiểm soát quy trình công việc",
"process": "Quy trình đến quy trình làm việc",
"select": "Lựa chọn",
"upload": "Tải video lên"
},
"headings": {
"title": "Quy trình làm việc video"
},
"tabTitle": {
"library": "Chọn từ phương tiện truyền thông",
"original": "Nguyên bản",
"preview": "Xem trước",
"upload": "Tải lên"
}
}
@@ -20,5 +20,6 @@
"videoFaceAnimator": "脸部动画师",
"videoMotionCapture": "视频动作捕捉",
"videoTitle": "视频生成",
"videoW2l": "视频唇形同步"
"videoW2l": "视频唇形同步",
"videoWorkflow": "视频工作流程"
}
@@ -0,0 +1,17 @@
{
"button": {
"proceed": "继续工作流程控制",
"process": "流程到工作流程",
"select": "选择",
"upload": "上传视频"
},
"headings": {
"title": "视频工作流程"
},
"tabTitle": {
"library": "从媒体中选择",
"original": "原来的",
"preview": "预览",
"upload": "上传"
}
}
+1 -1
View File
@@ -210,7 +210,7 @@ function initInferenceJobsByCategoryMap(): Map<
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoMotionCapture, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ImageGeneration, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.ConvertFbxtoGltf, []);
inferenceJobsByCategory.set(FrontendInferenceJobType.StorytellerFilter, [])
inferenceJobsByCategory.set(FrontendInferenceJobType.VideoWorkflow, [])
return inferenceJobsByCategory;
}
@@ -2,9 +2,9 @@
.fy-cover-img-input {
// background-color: red;
// position: relative;
// height: 0;
// padding-bottom: 100%;
position: relative;
height: 0;
padding-bottom: 100%;
.fy-cover-control {
position: absolute;
@@ -17,23 +17,23 @@
margin: auto auto 1rem auto;
}
// .weight-initial-cover-img {
// background-size: cover;
// background-position: center;
// border-radius: 0.5rem;
// position: absolute;
// // display: flex;
// overflow: hidden;
// height: 100%;
// width: 100%;
// // padding-bottom: 100%;
.weight-initial-cover-img {
background-size: cover;
background-position: center;
border-radius: 0.5rem;
position: absolute;
// display: flex;
overflow: hidden;
height: 100%;
width: 100%;
// padding-bottom: 100%;
// div {
// // background-color: red;
// display: flex;
// height: 100%;
// width: 100%;
// position: absolute;
// }
// }
div {
// background-color: red;
display: flex;
height: 100%;
width: 100%;
position: absolute;
}
}
}
@@ -14,24 +14,14 @@ export default function Label({
required,
disabled,
}: Props) {
return (
<div
{...{
className: `fy-input-label ${disabled ? "opacity-50" : ""}`.trim(),
}}
>
{label ? (
<label
{...{ className: `fw-medium ${required ? " required" : ""}`.trim() }}
>
{label}
</label>
) : null}
{invalidReason ? (
<span {...{ className: "label-error form-text red is-danger" }}>
{invalidReason}
</span>
) : null}
</div>
);
return label ? <div {...{ className: `fy-input-label${disabled ? "opacity-50" : "" }` }}>
<label {...{ className: `fw-medium${required ? " required" : "" }` }} >
{label}
</label>
{ invalidReason ? (
<span {...{ className: "label-error form-text red is-danger" }}>
{invalidReason}
</span>
) : null }
</div> : null;
}
@@ -2,28 +2,29 @@ import React, { useState, memo, useEffect } from "react";
import Searcher from "../Searcher";
import Modal from "../Modal";
import NonRouteTabs from "../Tabs/NonRouteTabs";
import Input from "../Input";
import Input from "../TempInput";
import Button from "../Button";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import SelectMediaList from "./SelectMediaList";
import SelectWeightsList from "./SelectWeightsList";
export type SelectModalData = {
token: string;
title: string;
}
interface TabConfig {
label: string;
tabKey: string;
type: "media" | "weights";
weightTypeFilter?: string;
mediaTypeFilter?: string;
typeFilter?: string;
searcher?: boolean;
}
interface SelectModalProps {
label?: string;
tabs: TabConfig[];
modalTitle?: string;
onSelect?: (data:{
token: string,
title: string,
}) => void;
value?: SelectModalData;
onSelect?: (data: SelectModalData) => void;
required?: boolean;
}
@@ -33,42 +34,44 @@ const SelectModal = memo(
tabs,
modalTitle = "Select",
onSelect,
value,
required,
}: SelectModalProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const initialValue = {token:"", title:""};
const [selectedValue, setSelectedValue] = useState(initialValue);
const [activeTab, setActiveTab] = useState(tabs[0].tabKey);
const [mediaType, setMediaType] = useState(
tabs[0].mediaTypeFilter || "all"
);
const [weightType, setWeightType] = useState(
tabs[0].weightTypeFilter || "all"
);
console.log("🔫", tabs, activeTab, weightType);
const emptyValue = {token:"", title:""};
const [{isModalOpen, selectedValue, valueType, activeTab}, setState] = useState({
isModalOpen:false,
selectedValue: value ? value : emptyValue,
activeTab: tabs[0].tabKey,
valueType: tabs[0].typeFilter || "all"
})
console.log("🔫", tabs, activeTab, valueType);
// Update mediaType when activeTab changes
useEffect(() => {
const currentTab = tabs.find(tab => tab.tabKey === activeTab);
setMediaType(currentTab?.mediaTypeFilter || "all");
setWeightType(currentTab?.weightTypeFilter || "all");
setState((curr)=>({...curr, valueType: currentTab?.typeFilter || "all"}))
}, [activeTab, tabs]);
const openModal = () => {
setIsModalOpen(true);
setState((curr)=>({...curr, isModalOpen: true}));
};
const closeModal = () => {
setIsModalOpen(false);
setState((curr)=>({...curr, isModalOpen: false}));
};
const handleRemove = () => {
setSelectedValue(initialValue);
setState((curr)=>({...curr, selectedValue: emptyValue}));
if (onSelect) onSelect(emptyValue);
};
const handleOnSelect = (data:{token:string, title:string}) => {
setSelectedValue({token: data.token, title: data.title || ""});
setState((curr)=>({
...curr,
selectedValue: {token: data.token, title: data.title || ""},
isModalOpen: false
}));
if (onSelect) onSelect(data);
closeModal();
}
const searchTabs = tabs.map(tab => ({
@@ -78,20 +81,20 @@ console.log("🔫", tabs, activeTab, weightType);
type="modal"
onResultSelect={handleOnSelect}
searcherKey={tab.tabKey}
weightType={tab.weightTypeFilter}
weightType={tab.typeFilter}
/>
) : (
<>
{tab.type === "media" && (
<SelectMediaList
mediaType={mediaType}
mediaType={valueType}
listKey={tab.tabKey}
onResultSelect={handleOnSelect}
/>
)}
{tab.type === "weights" && (
<SelectWeightsList
weightType={weightType}
weightType={valueType}
listKey={tab.tabKey}
onResultSelect={handleOnSelect}
/>
@@ -99,7 +102,7 @@ console.log("🔫", tabs, activeTab, weightType);
</>
),
padding: true,
onClick: () => setActiveTab(tab.tabKey),
onClick: () => setState((curr)=>({...curr, activeTab: tab.tabKey})),
}));
return (
@@ -111,15 +114,22 @@ console.log("🔫", tabs, activeTab, weightType);
</label>
)}
<div className="d-flex gap-2">
<div className="d-flex gap-2 position-relative">
<div
className="position-absolute w-100 h-100"
style={{"cursor": "pointer"}}
onClick={openModal}
/>
<Input
disabled={true}
className="w-100"
wrapperClassName="w-100"
placeholder="None selected"
onClick={openModal}
value={selectedValue.title !==""
? selectedValue.title
: selectedValue.token || ""}
/>
<Button label={selectedValue.token !== "" ? "Change" : "Select"} onClick={openModal} />
{selectedValue.token && (
<Button
@@ -5,6 +5,7 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import "./Input.scss";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
wrapperClassName ?: string;
className?: string;
icon?: IconDefinition;
invalidReason?: string;
@@ -14,6 +15,7 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
}
export default function TempInput({
wrapperClassName,
className,
label,
icon,
@@ -24,8 +26,8 @@ export default function TempInput({
}: InputProps) {
return (
// Changed fragment to div here just so that it can be laid out with bootstrap easily using d-flex, flex-column and responsive gaps which requires grouping.
<div className="fy-input">
<Label {...{ invalidReason, label, required }}/>
<div className={`fy-input${wrapperClassName ? " "+ wrapperClassName : ""}`}>
{label && <Label {...{ invalidReason, label, required }}/>}
<div className={`fy-input ${icon ? "input-icon" : ""}`}>
{icon && (
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
@@ -106,4 +106,14 @@
label { }
}
.fy-entity-input-upload-detail {
span {
opacity: .66;
display: block;
font-size: .75rem;
line-height: 1em;
height: .75rem;
}
}
}
@@ -8,7 +8,7 @@ import { FileWrapper, Label, Spinner } from "components/common";
import { EntityType, MediaFilterProp, WeightFilterProp } 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 { faDiagramSankey, faFile, faFileArrowUp, faGrid, faImage, faPersonWalking, faWaveform } 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";
@@ -17,6 +17,7 @@ import "./EntityInput.scss";
interface Props {
label?: string,
onChange?: any,
owner?: string,
mediaType?: MediaFilterProp,
aspectRatio?: "square" | "landscape" | "portrait",
weightType?: WeightFilterProp,
@@ -32,6 +33,7 @@ interface EmptySlideProps extends SlideProps {
inputProps?: any,
onSelect: any,
open: any,
owner: string,
user: any
};
@@ -64,24 +66,37 @@ const MocapInputFull = ({ media }: SlideProps) => {
</>;
};
const MediaPickerEmpty = ({ entityType, filterType, media, onSelect, open, inputProps, user }: EmptySlideProps) => {
const MediaPickerEmpty = ({ entityType, filterType, media, onSelect, open, owner, inputProps, user }: EmptySlideProps) => {
const browserClick = () => open({
component: MediaBrowser,
props: { entityType, filterType, onSelect, username: user?.username || "" }
props: { entityType, filterType, onSelect, owner, username: user?.username || "" }
});
const mediaIcons = () => {
switch (filterType) {
case "audio": return faWaveform;
case "image": return faImage;
case "bvh": return faPersonWalking;
default: return faFile;
}
}
return <>
<Icon {...{ className: "fy-entity-input-icon", icon: faPersonWalking }}/>
<Icon {...{ className: "fy-entity-input-icon", icon: [faFile,mediaIcons(),faDiagramSankey][entityType] }}/>
<div {...{ className: "fy-entity-input-empty-controls" }}>
<FileWrapper {...{ containerClass: "fy-entity-input-row", panelClass: "fy-entity-input-button", noStyle: true, ...inputProps }}>
<>
<Icon {...{ className: "fy-entity-input-label-icon", icon: faFileArrowUp }}/>
Upload, click or drag here
</>
</FileWrapper>
{ entityType === EntityType.media &&
<FileWrapper {...{ containerClass: "fy-entity-input-row", panelClass: "fy-entity-input-button", noStyle: true, ...inputProps }}>
<>
<Icon {...{ className: "fy-entity-input-label-icon", icon: faFileArrowUp }}/>
<div {...{ className: "fy-entity-input-upload-detail" }}>
Upload, click or drag here
{ filterType !== "all" && <span>{ filterType } files supported</span> }
</div>
</>
</FileWrapper> }
<div {...{ className: "fy-entity-input-row fy-entity-input-button", onClick: browserClick }}>
<Icon {...{ className: "fy-entity-input-label-icon", icon: faGrid }}/>
Choose from your media
Choose from your { ["","media","weights"][entityType] }
</div>
</div>
</>;
@@ -94,7 +109,7 @@ const AniMod = ({ animating, className, isLeaving, render: Render, style, ...res
<Render {...{ ...rest, animating }} />
</a.div>;
export default function EntityInput({ aspectRatio = "square", label, onChange, mediaType, weightType }: Props) {
export default function EntityInput({ aspectRatio = "square", label, onChange, owner, mediaType, weightType }: Props) {
const entityType = mediaType ? EntityType.media : weightType ? EntityType.weights : EntityType.unknown;
const filterType = mediaType || weightType || "all";
const { search } = useLocation();
@@ -149,7 +164,7 @@ export default function EntityInput({ aspectRatio = "square", label, onChange, m
return [
<AniMod {...{ render: MediaBusy, className: "fy-entity-input-busy", ...sharedProps }}/>,
<AniMod {...{ render: MocapInputFull, className: "fy-entity-input-full", ...sharedProps }}/>,
<AniMod {...{ render: MediaPickerEmpty, className: "fy-entity-input-empty", entityType, filterType, inputProps, onSelect, open, user, ...sharedProps }}/>
<AniMod {...{ render: MediaPickerEmpty, className: "fy-entity-input-empty", entityType, filterType, inputProps, onSelect, open, owner, user, ...sharedProps }}/>
][i];
})
@@ -11,6 +11,7 @@ export enum MediaFilters {
export enum WeightsFilters {
all,
hifigan_tt2,
rvc_v2,
sd_1,
"sd_1.5",
sdxl,
@@ -35,6 +36,6 @@ export const EntityFilterOptions = ( mode: EntityType, t = (v:string) => v) => {
.filter(val => isNaN(Number(val)))
.map((value) => {
if (typeof value === "string") return { value, label: t(value) }
return { label: "", value: "" };
return { label: "all", value: "all" };
}) : [];
};
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import { ImagePreview, MocapPreview, VideoPreview } from '../CardPreviews';
import { AudioCard, OverlayCard, CardWrapper } from "components/entities";
import { AudioCard, OverlayCard, CardWrapper, WeightCard } from "components/entities";
import { EntityType } from "components/entities/EntityTypes";
interface MediaCardsProps {
@@ -33,6 +33,10 @@ const MediaCards = ({ props, type }: MediaCardsProps) => {
const WeightsCards = ({ props, type }: MediaCardsProps) => {
switch (type) {
case "rvc_v2":
return <CardWrapper {...{ ...props, card: WeightCard, padding: true }}/>;
case "tt2":
return <CardWrapper {...{ ...props, card: WeightCard, padding: true }}/>;
default:
return <div>Unsupported media type</div>;
}
@@ -0,0 +1,54 @@
import React from "react";
import moment from "moment";
import { MakeRatingsProps } from "hooks/useRatings";
import WeightCoverImage from "components/common/WeightCoverImage";
import CardBadge from "../CardBadge";
import CardFooter from "../CardFooter";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
interface Props {
data: any,
makeRatingsProps?: MakeRatingsProps,
showCover?: boolean
showCreator?: boolean
}
export default function WeightCard({ data, makeRatingsProps, showCover = true, showCreator }: Props) {
const { cover_image, created_at, details, maybe_creator, title, token, weight_type } = data || {}
const timeCreated = moment( created_at || "").fromNow();
const path = cover_image.maybe_cover_image_public_bucket_path || details?.maybe_weight_data?.maybe_cover_image_public_bucket_path;
const bucketConfig = new BucketConfig();
const coverImage = bucketConfig.getCdnUrl(path, 110, 100);
return <>
<div className="d-flex">
{showCover && (
<WeightCoverImage
src={coverImage}
height={110}
width={110}
coverIndex={cover_image?.default_cover?.image_index}
/>
)}
<div className="flex-grow-1">
<CardBadge {...{ className: `fy-entity-type${ weight_type ? "-" + weight_type : "" }`, label: weight_type }}/>
<div className="d-flex align-items-center mt-3">
<div className="flex-grow-1">
<h6 className="fw-semibold text-white mb-1">
{ title || details.maybe_weight_data.title}
</h6>
<p className="fs-7 opacity-75">{ timeCreated }</p>
</div>
</div>
</div>
</div>
<CardFooter {...{
creator: maybe_creator,
entityToken: token,
entityType: "media_file",
makeRatingsProps
}}/>
</>;
};
@@ -0,0 +1 @@
export { default } from "./WeightCard";
@@ -3,5 +3,6 @@ import CardWrapper from "./CardWrapper";
import EntityInput from "./EntityInput";
import MediaList from "./MediaList";
import OverlayCard from "./OverlayCard";
import WeightCard from "./WeightCard";
export { AudioCard, CardWrapper, EntityInput, MediaList, OverlayCard };
export { AudioCard, CardWrapper, EntityInput, MediaList, OverlayCard, WeightCard };
@@ -271,6 +271,96 @@ export default function SideNav({
queueStats.legacy_tts.pending_job_count +
queueStats.inference.by_queue.pending_tacotron2_jobs;
let maybeVideoGeneration = <></>;
if (sessionWrapper.canAccessStudio()) {
maybeVideoGeneration = (
<>
<li>
<NavLink
to="/video-mocap"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faPersonRays}
className="sidebar-heading-icon"
/>
{t("videoMotionCapture")}
</NavLink>
</li>
<li>
<NavLink
to="/video-workflow"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faArrowsTurnToDots}
className="sidebar-heading-icon"
/>
{t("videoWorkflow")}
</NavLink>
</li>
<li>
<NavLink
to="/studio"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faCameraMovie}
className="sidebar-heading-icon"
/>
Storyteller Studio
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<li>
<NavLink
to="/engine-compositor"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faTransporter}
className="sidebar-heading-icon"
/>
Engine Compositor
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
</>
);
}
let maybeImageGeneration = <></>;
if (sessionWrapper.canAccessStudio()) {
maybeImageGeneration = (
<>
<li className="sidebar-heading">Image Generation</li>
<li>
<NavLink
to="/text-to-image"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faMessageImage}
className="sidebar-heading-icon"
/>
Text to Image
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<hr className="mb-3 mt-3" />
</>
);
}
return (
<>
<div id="sidebar-wrapper" className={sidebarClassName}>
@@ -372,80 +462,13 @@ export default function SideNav({
{t("videoFaceAnimator")}
</NavLink>
</li>
<li>
<NavLink
to="/video-mocap"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faPersonRays}
className="sidebar-heading-icon"
/>
{t("videoMotionCapture")}
</NavLink>
</li>
<li>
<NavLink
to="/video-workflow"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faArrowsTurnToDots}
className="sidebar-heading-icon"
/>
{t("videoWorkflow")}
</NavLink>
</li>
<li>
<NavLink
to="/studio"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faCameraMovie}
className="sidebar-heading-icon"
/>
Storyteller Studio
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<li>
<NavLink
to="/engine-compositor"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faTransporter}
className="sidebar-heading-icon"
/>
Engine Compositor
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
<hr className="mb-3 mt-3" />
<li className="sidebar-heading">Image Generation</li>
<li>
<NavLink
to="/text-to-image"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faMessageImage}
className="sidebar-heading-icon"
/>
Text to Image
{/* {t("videoStorytellerStudio")} */}
</NavLink>
</li>
{maybeVideoGeneration}
<hr className="mb-3 mt-3" />
{maybeImageGeneration}
<li className="sidebar-heading">{t("communityTitle")}</li>
<li>
<NavLink
@@ -20,12 +20,12 @@ interface Props {
handleClose: any,
mediaToken: string,
onSelect?: any,
owner?: string,
filterType?: MediaFilterProp | WeightFilterProp,
username: string,
}
export default function MediaBrowser({ entityType, filterType: inputFilter, mediaToken, handleClose = () => {}, onSelect, username }: Props) {
console.log("😎",entityType);
export default function MediaBrowser({ entityType, filterType: inputFilter, mediaToken, handleClose = () => {}, onSelect, owner, username }: Props) {
const ratings = useRatings();
const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [filterType, filterTypeSet] = useState(inputFilter || "all");
@@ -46,7 +46,7 @@ export default function MediaBrowser({ entityType, filterType: inputFilter, medi
setShowMasonryGrid(true);
},
requestList: true,
urlParam: "echelon",
urlParam: owner || username,
urlUpdate: false
});
@@ -74,7 +74,7 @@ export default function MediaBrowser({ entityType, filterType: inputFilter, medi
return <>
<header {...{ className: "fy-media-browser-header" }}>
<div {...{ className: "fy-media-browser-tools" }}>
<h3>{ ["","Media","Weights"][entityType] }</h3>
<h3>{ ["",`${ owner ? owner + "'s " : "" }Media`,"Weights"][entityType] }</h3>
<Icon {...{ className: "icon-close-button", icon: faXmark, onClick: () => handleClose() }}/>
</div>
<div {...{ className: "fy-media-browser-tools" }}>
@@ -13,10 +13,7 @@ interface ModalConfig {
export default function ModalProvider({ children }: Props) {
const [modalState,modalStateSet] = useState<ModalConfig | null>(null);
const open = (cfg: ModalConfig) => {
console.log("🪬",cfg);
modalStateSet(cfg);
};
const open = (cfg: ModalConfig) => modalStateSet(cfg);
const close = () => modalStateSet(null);
return <ModalContext.Provider {...{ value: { close, open, modalState } }}>
@@ -5,6 +5,7 @@ import { InferenceJobsContext } from 'context';
export default function useInferenceJobs(jobType: FrontendInferenceJobType) {
const { byCategory, enqueue } = useContext(InferenceJobsContext);
return {
enqueue,
inferenceJobs: (byCategory?.get(jobType) || []),
@@ -33,7 +33,6 @@ export default function useListContent({
urlParam = "",
urlUpdate = true
}: Props) {
console.log("💎",fetcher);
const { pathname, search: locSearch } = useLocation();
const history = useHistory();
const urlQueries = new URLSearchParams(locSearch);
@@ -1,7 +1,8 @@
import { useState } from "react";
import { UploadModel } from "@storyteller/components/src/api/image_generation/UploadModel";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
import { useCoverImgUpload } from "hooks";
import { useCoverImgUpload, useInferenceJobs } from "hooks";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import { v4 as uuidv4 } from "uuid";
// this hook is mostly for organizational purposes while I work -V
@@ -13,6 +14,7 @@ export default function useLoraUpload() {
const [descriptionMD, descriptionMDSet] = useState("");
const [writeStatus, writeStatusSet] = useState(FetchStatus.paused);
const coverImg = useCoverImgUpload();
const { enqueue } = useInferenceJobs(FrontendInferenceJobType.ImageGeneration);
const onChange = ({ target }: { target: { name: string; value: any } }) => {
const todo: { [key: string]: (x: any) => void } = {
@@ -38,9 +40,11 @@ export default function useLoraUpload() {
visibility,
})
.then((res: any) => {
writeStatusSet(FetchStatus.success);
console.log("🌠", res);
// history.replace(`/weight/${token}`);
if (res.success && res.inference_job_token) {
console.log("🌠", res);
writeStatusSet(FetchStatus.success);
enqueue(res.inference_job_token, FrontendInferenceJobType.ImageGeneration);
}
})
.catch(err => {
writeStatusSet(FetchStatus.error);
@@ -71,17 +71,16 @@ export default function useWeightFetch({ onRemove = n, onSuccess = n, token }: P
statusSet(FetchStatus.in_progress);
GetWeight(token, {})
.then((res: any) => {
let {
creator_set_visibility,
description_markdown,
title: resTitle,
} = res;
statusSet(FetchStatus.success);
titleSet(resTitle);
descriptionMDSet(description_markdown);
visibilitySet(creator_set_visibility);
onSuccess(res);
setData(res);
if (res.success) {
let { creator_set_visibility, description_markdown, title: resTitle } = res;
statusSet(FetchStatus.success);
titleSet(resTitle);
descriptionMDSet(description_markdown);
visibilitySet(creator_set_visibility);
onSuccess(res);
setData(res);
} else { statusSet(FetchStatus.error); }
})
.catch(err => {
statusSet(FetchStatus.error);
@@ -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 StudioNotAvailable() {
const subText = (
<p> Please visit our <DiscordLink2 /> to join the wait list. </p>
);
return (
<Container type="panel" className="mb-5">
<PageHeader
title={"Studio Features Not Available"}
subText={subText}
/>
</Container>
);
}
@@ -1,12 +1,10 @@
.fy-engine-compositor {
// margin: 1rem;
padding: 1rem;
.engine-compositor-container {
padding: 1rem;
header {
// background: blue;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
@@ -14,16 +12,12 @@
width: 100%;
}
h2 { margin-bottom: 0; }
h2, .fy-temp-select, .fy-input { margin-bottom: 0; }
}
.compositor-content {
display: flex;
gap: 1rem;
// padding: 1rem;
> * {
width: 100%;
// background: red;
}
> * { width: 100%; }
}
}
@@ -6,12 +6,15 @@ import { EnqueueEngineCompositing } from "@storyteller/components/src/api/engine
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import { v4 as uuidv4 } from "uuid";
import "./EngineCompositor.scss"
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
interface Props {
value?: any;
sessionWrapper: SessionWrapper;
}
export default function EngineCompositor({ value }: Props) {
export default function EngineCompositor({ value, sessionWrapper }: Props) {
const [mediaToken,mediaTokenSet] = useState();
const onChange = ({ target }: any) => mediaTokenSet(target.value);
const inferenceJobs = useInferenceJobs(FrontendInferenceJobType.EngineComposition);
@@ -23,12 +26,10 @@ export default function EngineCompositor({ value }: Props) {
})
.then((res: any) => {
if (res && res.success) {
console.log("🚛 success!",{ mediaToken, res });
inferenceJobs.enqueue( // I need to pass this
res.inference_job_token,
FrontendInferenceJobType.EngineComposition
);
}
});
}
@@ -40,6 +41,10 @@ export default function EngineCompositor({ value }: Props) {
}
};
if (!sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
return <div {...{ className: "fy-engine-compositor"}}>
<div {...{ className: "panel engine-compositor-container" }}>
<header>
@@ -89,6 +89,9 @@ function AboutPage(props: Props) {
},
];
// NB: just to prevent yarn lints from complaining
console.log(teamMembers.length);
return (
<Container type="panel" className="mb-5">
<PageHeaderWithImage
@@ -97,7 +100,7 @@ function AboutPage(props: Props) {
headerImage="/mascot/kitsune_pose3.webp"
yOffset="85%"
/>
<Panel padding={true} mb={true}>
{/*<Panel padding={true} mb={true}>
<h2 className="mb-3 fw-bold">Lorem Ipsum</h2>
<p>
@@ -109,7 +112,7 @@ function AboutPage(props: Props) {
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</Panel>
</Panel>*/}
<Panel padding={true} mb={true}>
<h2 className="mb-3 fw-bold">Our Mission</h2>
@@ -123,6 +126,7 @@ function AboutPage(props: Props) {
</p>
</Panel>
{/*
<Panel padding={true}>
<h2 className="mb-3 fw-bold">The Team</h2>
@@ -184,6 +188,7 @@ function AboutPage(props: Props) {
))}
</div>
</Panel>
*/}
</Container>
);
}
@@ -1,11 +1,7 @@
import React, { useState } from "react";
import { EntityInput } from "components/entities";
import { Button } from "components/common";
// import { useInferenceJobs } from "hooks";
// import InferenceJobsList from "components/layout/InferenceJobsList";
// import { EnqueueEngineCompositing } from "@storyteller/components/src/api/engine_compositor/EnqueueEngineCompositing";
// import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
// import { v4 as uuidv4 } from "uuid";
import { EntityFilterOptions, EntityType, MediaFilterProp, WeightFilterProp } from "components/entities/EntityTypes";
import { Button, SegmentButtons, TempInput, TempSelect } from "components/common";
interface Props {
value?: any;
@@ -13,34 +9,34 @@ interface Props {
export default function DevMediaInput({ value }: Props) {
const [mediaToken,mediaTokenSet] = useState();
const [mediaType,mediaTypeSet] = useState<MediaFilterProp>("all");
const [weightType,weightTypeSet] = useState<WeightFilterProp>("all");
const [entityType,entityTypeSet] = useState(EntityType.media);
const [owner,ownerSet] = useState("");
const onChange = ({ target }: any) => mediaTokenSet(target.value);
// const inferenceJobs = useInferenceJobs(FrontendInferenceJobType.EngineComposition);
// const failures = (fail = "") => {
// switch (fail) {
// default:
// return "Uknown failure";
// }
// };
const options = [{ label: "Media", value: EntityType.media },{ label: "Weights", value: EntityType.weights }];
const changeFilter = ({ target }: { target: any }) => [mediaTypeSet,mediaTypeSet,weightTypeSet][entityType](target.value);
return <div {...{ className: "fy-engine-compositor"}}>
<div {...{ className: "panel engine-compositor-container" }}>
<header>
<h2>{ [526,187].map((num = 0) => String.fromCodePoint(128000 + num)) }</h2>
<SegmentButtons {...{ onChange: ({ target }: { target: any }) => entityTypeSet(target.value), options, value: entityType }}/>
<TempInput {...{ value: owner, onChange: ({ target }: { target: any }) => ownerSet(target.value), placeholder: "User" }}/>
<TempSelect {...{ options: EntityFilterOptions(entityType), value: ["",mediaType,weightType][entityType], onChange: changeFilter }}/>
<Button {...{ label: "Enqueue", variant: "primary" }}/>
</header>
<EntityInput {...{ aspectRatio: "landscape", label: "Choose Media File", onChange, weightType: "sd_1.5" }}/>
<EntityInput {...{
aspectRatio: "landscape",
label: `Choose ${ ["","media file","weight"][entityType] }`,
onChange,
owner,
...([{},{ mediaType },{ weightType }][entityType])
}}/>
<div>
{ mediaToken }
</div>
</div>
{
// <InferenceJobsList
// {...{
// failures,
// jobType: FrontendInferenceJobType.EngineComposition,
// }}
// />
}
</div>;
};
@@ -5,6 +5,7 @@ import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { Container, Panel } from "components/common";
import { faCameraMovie } from "@fortawesome/pro-solid-svg-icons";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
interface Props {
sessionWrapper: SessionWrapper;
@@ -12,6 +13,10 @@ interface Props {
}
function StorytellerStudioListPage(props: Props) {
if (!props.sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
return (
<Container type="panel">
<PageHeaderWithImage
@@ -7,9 +7,12 @@ import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session
import { faMessageImage } from "@fortawesome/pro-solid-svg-icons";
import TextToImageJobsList from "./components/TextToImageJobsList";
import { useInferenceJobs } from "hooks";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
interface TextToImagePageProps {
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
sessionWrapper: SessionWrapper;
enqueueInferenceJob: (
jobToken: string,
frontendInferenceJobType: FrontendInferenceJobType
@@ -17,6 +20,7 @@ interface TextToImagePageProps {
}
export default function TextToImagePage({
sessionWrapper,
sessionSubscriptionsWrapper,
enqueueInferenceJob,
}: TextToImagePageProps) {
@@ -25,6 +29,10 @@ export default function TextToImagePage({
);
const hasImageGenJobs = inferenceJobs && inferenceJobs.length > 0;
if (!sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
return (
<Container type="panel">
<PageHeaderWithImage
@@ -15,6 +15,8 @@ import {
TempSelect,
TempTextArea,
} from "components/common";
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
import InferenceJobsList from "components/layout/InferenceJobsList";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import useLoraUpload from "hooks/useLoraUpload";
@@ -32,6 +34,14 @@ export default function UploadLoraWeightPage({
{ label: "Private", value: "private" },
];
const failures = (fail = "") => {
switch (fail) {
default:
return "Uknown failure";
}
};
const {
coverImg,
descriptionMD,
@@ -66,6 +76,12 @@ export default function UploadLoraWeightPage({
return (
<Container type="panel">
<InferenceJobsList
{...{
failures,
jobType: FrontendInferenceJobType.ImageGeneration,
}}
/>
<PageHeader
title="Upload LoRA Weight"
titleIcon={faWaveform}
@@ -21,6 +21,8 @@ import {
faPersonRays,
} from "@fortawesome/pro-solid-svg-icons";
import PageHeaderWithImage from "components/layout/PageHeaderWithImage";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
export default function VideoMotionCapture(props: {
enqueueInferenceJob: (
@@ -29,8 +31,9 @@ export default function VideoMotionCapture(props: {
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
sessionWrapper: SessionWrapper;
}) {
const { enqueueInferenceJob } = props;
const { enqueueInferenceJob, sessionWrapper } = props;
const { t } = useLocalize("VideoMotionCapture");
const { NO_FILE, FILE_UPLOADING, MOCAPNET_ENQUEUED } = states;
const [pageState, dispatchPageState] = useReducer(reducer, {
@@ -59,6 +62,10 @@ export default function VideoMotionCapture(props: {
}, [pageState, enqueueInferenceJob]);
const { pathname } = useLocation();
if (!sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
if (pathname === `/video-mocap` || pathname === `/video-mocap/`) {
return <Redirect to={`/video-mocap/upload`} />;
}
@@ -21,7 +21,7 @@ export default function PageVideoMocapProgress({
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const { FILE_UPLOADING, FILE_UPLOADED, FILE_SELECTED, MOCAPNET_ENQUEUEING } = states;
const { FILE_UPLOADING, FILE_UPLOADED, FILE_SELECTED_PROCEED, MOCAPNET_ENQUEUEING } = states;
const handleEnqueueMocapNet = () => {
if (pageState.mediaFileToken) {
@@ -43,7 +43,7 @@ export default function PageVideoMocapProgress({
}
};
if (pageState.status === FILE_SELECTED){
if (pageState.status === FILE_SELECTED_PROCEED){
handleEnqueueMocapNet();
}
@@ -1,5 +1,6 @@
import SelectModal from "components/common/SelectModal/SelectModal";
import React, { useState } from "react";
import React from "react";
import SelectModal, {SelectModalData} from "components/common/SelectModal/SelectModal";
import { Action, State } from "../videoMocapReducer";
import { Button } from "components/common";
@@ -12,35 +13,45 @@ export default function TabContentLibrary({
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const [token, setToken] = useState<string | undefined>();
const handleProceed = () => {
if (token)
dispatchPageState({ type: "proceedSelectedFile" });
};
const handleOnSelect = (data:SelectModalData) => {
if (data.token !==""){
dispatchPageState({
type: "selectedFile",
payload: { mediaFileToken: token },
payload: { mediaFileToken: data.token },
});
};
const handleOnSelect = (data:{token:string,title:string}) => {
setToken(data.token);
}else{
dispatchPageState({type: "clearedFile"});
}
};
return (
<div>
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
tabs={[
{
label: "All Videos",
tabKey: "allVideos",
mediaTypeFilter: "video",
searcher: false,
type: "media",
},
]}
/>
{token && <Button label={t("button.proceed")} onClick={handleProceed} />}
<div className="row g-3">
<div className="col-12">
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
value={{token: pageState.mediaFileToken || "", title: ""}}
tabs={[
{
label: "All Videos",
tabKey: "allVideos",
typeFilter: "video",
searcher: false,
type: "media",
},
]}
/>
</div>
{pageState.mediaFileToken &&
<div className="col-12 d-flex justify-content-center mt-5">
<Button label={t("button.proceed")} onClick={handleProceed} />
</div>
}
</div>
);
}
@@ -13,7 +13,7 @@ export default function TabContentUpload({t, pageState, dispatchPageState}: {
dispatchPageState: (action: Action) => void;
}) {
const videoProps = useFile({});
const {NO_FILE, FILE_STAGED} = states;
const {NO_FILE, FILE_STAGED, FILE_UPLOADING} = states;
const makeVideoUploadRequest = () => ({
@@ -38,7 +38,7 @@ export default function TabContentUpload({t, pageState, dispatchPageState}: {
};
// contains upload inout state and controls, see docs
if (pageState.status === NO_FILE || pageState.status === FILE_STAGED) {
if (pageState.status < FILE_UPLOADING) {
return (
<>
<div className="row">
@@ -1,10 +1,11 @@
export enum states{
NO_FILE,
FILE_SELECTED,
FILE_SELECTED_PROCEED,
FILE_STAGED,
FILE_UPLOADING,
FILE_UPLOADED,
FILE_SELECTED,
MOCAPNET_ENQUEUEING,
MOCAPNET_ENQUEUED,
}
@@ -20,6 +21,7 @@ export type Action =
| {type: 'stagedFile'}
| {type: 'clearedFile'}
| {type: 'selectedFile', payload:{ mediaFileToken: string}}
| {type: 'proceedSelectedFile'}
| {type: 'uploadFile'}
| {type: 'uploadFileSuccess', payload:{ mediaFileToken: string}}
| {type: 'enqueueMocapNet'}
@@ -32,13 +34,19 @@ export function reducer (state: State, action: Action): State {
case 'stagedFile':
return {...state, status: states.FILE_STAGED}
case 'clearedFile':
return {...state, status: states.NO_FILE}
return {
...state,
status: states.NO_FILE,
mediaFileToken: undefined
}
case 'selectedFile':
return {
...state,
status: states.FILE_SELECTED,
mediaFileToken: action.payload.mediaFileToken
}
case 'proceedSelectedFile':
return {...state, status: states.FILE_SELECTED_PROCEED }
case 'uploadFile':
return {...state,status: states.FILE_UPLOADING};
case 'uploadFileSuccess':
@@ -8,6 +8,8 @@ import PageHeader from "components/layout/PageHeader";
import { states, reducer } from "./videoWorkflowReducer";
import SubRoutes from "./videoWorkflowRoutes";
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { StudioNotAvailable } from 'v2/view/_common/StudioNotAvailable';
export default function StorytellerFilter(props:{
enqueueInferenceJob: (
@@ -16,9 +18,10 @@ export default function StorytellerFilter(props:{
) => void;
inferenceJobs: Array<InferenceJob>;
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
sessionWrapper: SessionWrapper;
}){
const debug=true;
const {t} = useLocalize("StorytellerFilter");
const debug = true;
const {t} = useLocalize("VideoWorkflow");
const { NO_FILE } = states;
const [pageState, dispatchPageState] = useReducer(reducer, {
status: NO_FILE,
@@ -26,16 +29,16 @@ export default function StorytellerFilter(props:{
const { enqueueInferenceJob } = props;
useInferenceJobs(
FrontendInferenceJobType.StorytellerFilter
FrontendInferenceJobType.VideoWorkflow
);
useEffect(() => {
if (
pageState.status === states.FILTER_ENQUEUED &&
pageState.status === states.WORKFLOW_ENQUEUED &&
pageState.inferenceJobToken
) {
enqueueInferenceJob(
pageState.inferenceJobToken,
FrontendInferenceJobType.StorytellerFilter
FrontendInferenceJobType.VideoWorkflow
);
dispatchPageState({
type: "enqueueFilterSuccess",
@@ -44,8 +47,13 @@ export default function StorytellerFilter(props:{
}
}, [pageState, enqueueInferenceJob]);
if (!props.sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
return(
<Container type="panel" className="mb-5">
{debug && <p>{`Status:${pageState.status} MediaToken:${pageState.mediaFileToken}`}</p>}
<PageHeader
title={t("headings.title")}
subText={t("headings.subtitle")}
@@ -1,472 +0,0 @@
import React, {useState} from "react";
import { useParams, useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMedia } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { EnqueueStorytellerFilter } from "@storyteller/components/src/api/storyteller_filter";
import { states, Action, State } from "../videoWorkflowReducer";
import {
Accordion,
Button,
BasicVideo,
Checkbox,
ErrorMessage,
Input,
InputSeed,
NumberSliderV2,
Panel,
SelectModal,
Spinner,
TextArea
} from "components/common";
export default function PageFilterControls({
t, pageState, dispatchPageState
}: {
debug?: boolean;
t: Function;
pageState: State;
dispatchPageState: (action: Action) => void;
}) {
const { mediaToken } = useParams<any>();
useMedia({
mediaToken: pageState.mediaFileToken || mediaToken,
onSuccess: (res: any) => {
dispatchPageState({
type: 'loadFileSuccess',
payload: {
mediaFile: res,
mediaFileToken: pageState.mediaFileToken || mediaToken
}
})
},
});
const [filterState, setFilterState] = useState({
outputPath: "vid2vid/SparseUpscaleInterp_00001.mp4",
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: "",
sdModelToken: "",
loraModelToken: "",
posPrompt: "",
negPrompt: "",
firstPass: 1,
upscalePass: 0.42,
motionScale:1,
upscaleMultiplier: 1.5,
useEmptyLatent: false,
useFaceDetailer: false,
denoiseFaceDetailer: 0.45,
useLCM: false,
lcmCFG: 2,
lcmSteps: 8,
framesCap:16,
skipFrames:0,
nthFrames:2,
inputFps: 24,
interpolationMutiplier: 2,
cnTile: 0,
cnCanny: 0,
cnLinearAnime: 0,
cnLinearRealistic: 0,
cnDepth: 0,
cnOpenPose: 0,
cnPipeFace: 0,
cnSparse: 0.7,
});
const handleOnChange = (key: string, newValue:any,) => {
setFilterState((curr)=>({...curr, [key]: newValue}));
}
const history = useHistory();
const handleGenerate = ()=>{
console.log("filterState:")
console.log(filterState)
const request = {
"uuid_idempotency_token": uuidv4(),
"maybe_sd_model": filterState.sdModelToken,
"maybe_workflow_config": filterState.workflowConfig,
"maybe_input_file": pageState.mediaFileToken || mediaToken,
"maybe_output_path": filterState.outputPath,
"maybe_json_modifications": {
"$.510.inputs.Text": filterState.posPrompt,
"$.8.inputs.text": filterState.negPrompt,
"$.173.inputs.seed": filterState.seed,
"$.401.inputs.Value": filterState.firstPass,
"$.918.inputs.Value": filterState.motionScale,
"$.137.inputs.Value": filterState.framesCap,
"$.186.inputs.Value": filterState.skipFrames,
"$.140.inputs.Value": filterState.nthFrames,
"$.154.inputs.Value": filterState.inputFps,
"$.445.inputs.number": filterState.interpolationMutiplier,
"$.947.inputs.Value": filterState.cnTile,
"$.800.inputs.Value": filterState.cnCanny,
"$.797.inputs.Value": filterState.cnLinearAnime,
"$.796.inputs.Value": filterState.cnLinearRealistic,
"$.772.inputs.Value": filterState.cnDepth,
"$.771.inputs.Value": filterState.cnOpenPose,
"$.527.inputs.Value": filterState.cnPipeFace,
"$.403.inputs.Value": filterState.cnSparse
},
}
EnqueueStorytellerFilter(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("/storyteller-filter/jobs");
}
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-5">
<BasicVideo src={mediaLink} />
</div>
<div className="col-1 d-flex align-items-center justify-items-center">
<FontAwesomeIcon icon={faChevronRight} className="fa-6x"/>
</div>
<div className="col-6">
<BasicVideo src={mediaLink} />
</div>
</div>
</Panel>
<Accordion className="mt-4">
<Accordion.Item title={"Basics"} defaultOpen>
<div className="row g-3 p-3">
<SelectModal
modalTitle="Select a Stable Diffusion Weight"
label="Select a Stable Diffusion Weight"
onSelect={({token})=>{
handleOnChange("sdModelToken", token);
}}
tabs={[
{
label: "All Weights",
tabKey: "allWeights",
weightTypeFilter: "sd_1.5",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedWeights",
weightTypeFilter: "sd_1.5",
searcher: false,
type: "weights",
},
]}
/>
</div>
<div className="row g-3 p-3">
<SelectModal
modalTitle="Select a LoRA Weight"
label="Additional LoRA Weight"
onSelect={({token})=>{
console.log(`calling from select modal result select ${token}`);
handleOnChange("loraModelToken", token);
}}
tabs={[
{
label: "All LoRA Weights",
tabKey: "allLoraWeights",
weightTypeFilter: "rvc_v2",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedLoraWeights",
weightTypeFilter: "rvc_v2",
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: filterState.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: filterState.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>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 1, step: 0.01,
initialValue: filterState.firstPass,
label: " Denoise First Pass",
thumbTip: "Denoise First Pass",
onChange: (val)=>{handleOnChange("firstPass",val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 1, step: 0.01,
initialValue: filterState.upscalePass,
label: "Denoise Upscale Pass",
thumbTip: "Denoise Upscale Pass",
onChange: (val)=>{handleOnChange("upscalePass", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.25, max: 2, step: 0.05,
initialValue: filterState.motionScale,
label: "Motion Scale",
thumbTip: "Motion Scale",
onChange: (val)=>{handleOnChange("motionScale", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 1.5, max: 2, step: 0.5,
initialValue: filterState.upscaleMultiplier,
label: "Upscale Multiplier",
thumbTip: "Upscale Multiplier",
onChange: (val)=>{handleOnChange("upscaleMultiplier", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Checkbox
label="Use Empty Latent"
checked={filterState.useEmptyLatent}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useEmptyLatent", e.target.checked)}}
/>
</div>
<div className="col-md-6">
<Checkbox
label="Use Face Detailer"
checked={filterState.useFaceDetailer}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useFaceDetailer", e.target.checked)}}
/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 0.7, step: 0.01,
initialValue: filterState.denoiseFaceDetailer,
label: "Denoise Face Detailer",
thumbTip: "Denoise Face Detailer",
onChange: (val)=>{handleOnChange("denoiseFaceDetailer", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<Checkbox
label="Use LCM"
checked={filterState.useLCM}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useLCM", e.target.checked)}}
/>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 1, max: 10, step: 1,
initialValue: filterState.lcmCFG,
label: "LCM CFG",
thumbTip: "LCM CFG",
onChange: (val)=>{handleOnChange("lcmCFG", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 5, max: 15, step: 1,
initialValue: filterState.lcmSteps,
label: "LCM Steps",
thumbTip: "LCM Steps",
onChange: (val)=>{handleOnChange("lcmSteps", val)}
}}/>
</div>
</div>
</Accordion.Item>
<Accordion.Item title="Control Nets" defaultOpen>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnCanny,
label: "Canny",
thumbTip: "Canny",
onChange: (val)=>{handleOnChange("cnCanny", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnLinearAnime,
label: "Line Art Anime",
thumbTip: "Line Art Anime",
onChange: (val)=>{handleOnChange("cnLinearAnime", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnDepth,
label: "Depth",
thumbTip: "Depth",
onChange: (val)=>{handleOnChange("cnDepth", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnOpenPose,
label: "OpenPose",
thumbTip: "OpenPose",
onChange: (val)=>{handleOnChange("cnOpenPose", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnPipeFace,
label: "Media Pipe Face",
thumbTip: "Media Pipe Face",
onChange: (val)=>{handleOnChange("cnPipeFace", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnSparse,
label: "Sparse Scribble",
thumbTip: "Sparse Scribble",
onChange: (val)=>{handleOnChange("cnSparse", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnLinearRealistic,
label: "Video CN (Linear Realistic)",
thumbTip: "Video CN",
onChange: (val)=>{handleOnChange("cnLinearRealistic", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: filterState.cnTile,
label: "Tile CN",
thumbTip: "Tile CN",
onChange: (val)=>{handleOnChange("cnTile", val)}
}}/>
</div>
</div>
</Accordion.Item>
<Accordion.Item title="Video Settings">
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Width" />
</div>
<div className="col-md-6">
<Input label="Height" />
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Frames Cap" />
</div>
<div className="col-md-6">
<Input label="Skip Frames" />
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Every n-th Frame" />
</div>
<div className="col-md-6">
<Input label="Input FPS" readOnly/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Interpolation Multiplier" />
</div>
<div className="col-md-6">
</div>
</div>
</Accordion.Item>
</Accordion>
<div className="row g-3 py-3">
<div className="col-12 d-flex justify-content-end">
<Button
label={t("Process Filter")}
onClick={handleGenerate}
variant="primary"
/>
</div>
</div>
</>
);
}else if (pageState.status <= states.FILE_LOADING){
return (
<Panel>
<p>Loading Files</p>
<Spinner />
</Panel>
);
}
return <ErrorMessage />;
}
@@ -0,0 +1,8 @@
export function isInputValid(keyValues: {[key:string]:number|string|boolean|undefined}){
return (
keyValues.sdModelToken
&& keyValues.sdModelToken !== ""
&& keyValues.posPrompt
&& keyValues.posPrompt !== ""
);
}
@@ -0,0 +1 @@
export { default } from "./pageWorkflowControls";
@@ -0,0 +1,270 @@
import React, {useState, useRef} from "react";
import { useParams, useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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,
BasicVideo,
ErrorMessage,
InputSeed,
Panel,
SelectModal,
Spinner,
TextArea
} from "components/common";
import SectionControlNets, {ControlNetsInitialValues as CnIvs} from "./sectionControlNets";
import SectionVideoSettings from "./sectionVideoSettings";
import SectionAdvanceOptions, {AdvanceOptionsInitialValues as AdvIvs} from "./sectionAdvanceOptions";
import { isInputValid } 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({
outputPath: "vid2vid/SparseUpscaleInterp_00001.mp4",
workflowConfig: "weight_q8sz47gmfw2zx02snrbz88ns9",
seed: "",
sdModelToken: "",
loraModelToken: "",
posPrompt: "",
negPrompt: "",
framesCap:16,
skipFrames:0,
nthFrames:2,
inputFps: 24,
interpolationMutiplier: 2,
...AdvIvs,
...CnIvs
});
const handleOnChange = (key: string, newValue:any,) => {
setworkflowValues((curr)=>({...curr, [key]: newValue}));
}
const history = useHistory();
const handleGenerate = ()=>{
if(debug) console.log(workflowValues)
if (isInputValid(workflowValues)){
const request = {
"uuid_idempotency_token": uuidv4(),
"maybe_sd_model": workflowValues.sdModelToken,
"maybe_workflow_config": workflowValues.workflowConfig,
"maybe_input_file": pageState.mediaFileToken || mediaToken,
"maybe_output_path": workflowValues.outputPath,
"maybe_json_modifications": {
"$.510.inputs.Text": workflowValues.posPrompt,
"$.8.inputs.text": workflowValues.negPrompt,
"$.173.inputs.seed": workflowValues.seed,
"$.401.inputs.Value": workflowValues.firstPass,
"$.918.inputs.Value": workflowValues.motionScale,
"$.137.inputs.Value": workflowValues.framesCap,
"$.186.inputs.Value": workflowValues.skipFrames,
"$.140.inputs.Value": workflowValues.nthFrames,
"$.154.inputs.Value": workflowValues.inputFps,
"$.445.inputs.number": workflowValues.interpolationMutiplier,
"$.947.inputs.Value": workflowValues.cnTile,
"$.800.inputs.Value": workflowValues.cnCanny,
"$.797.inputs.Value": workflowValues.cnLinearAnime,
"$.796.inputs.Value": workflowValues.cnLinearRealistic,
"$.772.inputs.Value": workflowValues.cnDepth,
"$.771.inputs.Value": workflowValues.cnOpenPose,
"$.527.inputs.Value": workflowValues.cnPipeFace,
"$.403.inputs.Value": workflowValues.cnSparse
},
}
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-5">
<div className="fy-basic-video">
<video controls ref={videoRef}>
<source src={mediaLink} type="video/mp4"/>
</video>
</div>
</div>
<div className="col-2 d-flex align-items-center justify-content-center">
<FontAwesomeIcon icon={faChevronRight} className="fa-7x"/>
</div>
<div className="col-5">
<BasicVideo src={mediaLink} />
</div>
</div>
</Panel>
<Accordion className="mt-4">
<Accordion.Item title="Video Settings">
<SectionVideoSettings
onChange={(key,val)=>handleOnChange(key,val)}
videoElement={videoRef?.current}
/>
</Accordion.Item>
<Accordion.Item title={"Basics"} defaultOpen>
<div className="row g-3 p-3">
<SelectModal
modalTitle="Select a Stable Diffusion Weight"
label="Select a Stable Diffusion Weight"
onSelect={({token})=>{
handleOnChange("sdModelToken", token);
}}
tabs={[
{
label: "All Weights",
tabKey: "allWeights",
typeFilter: "sd_1.5",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedWeights",
typeFilter: "sd_1.5",
searcher: false,
type: "weights",
},
]}
/>
</div>
<div className="row g-3 p-3">
<SelectModal
modalTitle="Select a LoRA Weight"
label="Additional LoRA Weight"
onSelect={({token})=>{
console.log(`calling from select modal result select ${token}`);
handleOnChange("loraModelToken", token);
}}
tabs={[
{
label: "All LoRA Weights",
tabKey: "allLoraWeights",
typeFilter: "rvc_v2",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedLoraWeights",
typeFilter: "rvc_v2",
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={(key,val)=>handleOnChange(key,val)}
/>
</Accordion.Item>
<Accordion.Item title="Control Nets" defaultOpen>
<SectionControlNets
workflowValues={workflowValues}
onChange={(key,val)=>handleOnChange(key,val)}
/>
</Accordion.Item>
</Accordion>
<div className="row g-3 py-3">
<div className="col-12 d-flex justify-content-end">
<Button
label={t("Process Filter")}
onClick={handleGenerate}
variant="primary"
/>
</div>
</div>
</>
);
}else if (pageState.status <= states.FILE_LOADING){
return (
<Panel>
<p>Loading Files</p>
<Spinner />
</Panel>
);
}
return <ErrorMessage />;
}
@@ -0,0 +1,126 @@
import React, { memo } from 'react'
import { NumberSliderV2, Checkbox } from 'components/common'
export const AdvanceOptionsInitialValues = {
firstPass: 1,
upscalePass: 0.42,
motionScale:1,
upscaleMultiplier: 1.5,
useEmptyLatent: false,
useFaceDetailer: false,
denoiseFaceDetailer: 0.45,
useLCM: false,
lcmCFG: 2,
lcmSteps: 8,
}
export default memo( function SectionAdvanceOptions({
workflowValues: wfVal,
onChange : handleOnChange
} : {
workflowValues : any
onChange: (key:string, val:number|boolean)=>void}
){
return(
<>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 1, step: 0.01,
initialValue: wfVal.firstPass,
label: " Denoise First Pass",
thumbTip: "Denoise First Pass",
onChange: (val)=>{handleOnChange("firstPass",val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 1, step: 0.01,
initialValue: wfVal.upscalePass,
label: "Denoise Upscale Pass",
thumbTip: "Denoise Upscale Pass",
onChange: (val)=>{handleOnChange("upscalePass", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.25, max: 2, step: 0.05,
initialValue: wfVal.motionScale,
label: "Motion Scale",
thumbTip: "Motion Scale",
onChange: (val)=>{handleOnChange("motionScale", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 1.5, max: 2, step: 0.5,
initialValue: wfVal.upscaleMultiplier,
label: "Upscale Multiplier",
thumbTip: "Upscale Multiplier",
onChange: (val)=>{handleOnChange("upscaleMultiplier", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Checkbox
label="Use Empty Latent"
checked={wfVal.useEmptyLatent}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useEmptyLatent", e.target.checked)}}
/>
</div>
<div className="col-md-6">
<Checkbox
label="Use Face Detailer"
checked={wfVal.useFaceDetailer}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useFaceDetailer", e.target.checked)}}
/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0.1, max: 0.7, step: 0.01,
initialValue: wfVal.denoiseFaceDetailer,
label: "Denoise Face Detailer",
thumbTip: "Denoise Face Detailer",
onChange: (val)=>{handleOnChange("denoiseFaceDetailer", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<Checkbox
label="Use LCM"
checked={wfVal.useLCM}
onChange={(e:{target:{ checked: boolean, name:string, type: string }})=>{handleOnChange("useLCM", e.target.checked)}}
/>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 1, max: 10, step: 1,
initialValue: wfVal.lcmCFG,
label: "LCM CFG",
thumbTip: "LCM CFG",
onChange: (val)=>{handleOnChange("lcmCFG", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 5, max: 15, step: 1,
initialValue: wfVal.lcmSteps,
label: "LCM Steps",
thumbTip: "LCM Steps",
onChange: (val)=>{handleOnChange("lcmSteps", val)}
}}/>
</div>
</div>
</>
);
});
@@ -0,0 +1,107 @@
import React, { memo } from 'react'
import { NumberSliderV2 } from 'components/common'
export const ControlNetsInitialValues = {
cnCanny: 0,
cnDepth: 0,
cnLinearAnime: 0,
cnLinearRealistic: 0,
cnOpenPose: 0,
cnPipeFace: 0,
cnSparse: 0.7,
cnTile: 0,
}
export default memo( function SectionControlNets({
workflowValues: wfVal,
onChange : handleOnChange
} : {
workflowValues : any
onChange: (key:string, val:number)=>void}
){
return(
<>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnCanny,
label: "Canny",
thumbTip: "Canny",
onChange: (val)=>{handleOnChange("cnCanny", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnLinearAnime,
label: "Line Art Anime",
thumbTip: "Line Art Anime",
onChange: (val)=>{handleOnChange("cnLinearAnime", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnDepth,
label: "Depth",
thumbTip: "Depth",
onChange: (val)=>{handleOnChange("cnDepth", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnOpenPose,
label: "OpenPose",
thumbTip: "OpenPose",
onChange: (val)=>{handleOnChange("cnOpenPose", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnPipeFace,
label: "Media Pipe Face",
thumbTip: "Media Pipe Face",
onChange: (val)=>{handleOnChange("cnPipeFace", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnSparse,
label: "Sparse Scribble",
thumbTip: "Sparse Scribble",
onChange: (val)=>{handleOnChange("cnSparse", val)}
}}/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnLinearRealistic,
label: "Video CN (Linear Realistic)",
thumbTip: "Video CN",
onChange: (val)=>{handleOnChange("cnLinearRealistic", val)}
}}/>
</div>
<div className="col-md-6">
<NumberSliderV2 {...{
min: 0, max: 1, step: 0.1,
initialValue: wfVal.cnTile,
label: "Tile CN",
thumbTip: "Tile CN",
onChange: (val)=>{handleOnChange("cnTile", val)}
}}/>
</div>
</div>
</>
);
});
@@ -0,0 +1,80 @@
import React, { memo, useState } from 'react'
import { TempInput as Input } from 'components/common'
export const VideoSettingsInitialValues = {
width: 544,
height: 544,
framesCap: 16,
skipFrames: 0,
everyNthFrame: 2,
inputFps: 24,
interpolationMultiplier: 2,
}
export default memo( function SectionVideoSettings({
onChange : handleOnChange,
videoElement: ve
} : {
onChange: (key:string, val:number)=>void,
videoElement: HTMLVideoElement | null,
}){
const iv = VideoSettingsInitialValues;
const [{width, height}, setDimensions] = useState({
width:iv.width, height:iv.height
});
if(ve && ve!==null){
ve.onloadedmetadata = () =>{
if (ve.videoWidth && ve.videoHeight) {
const aspectRatio = ve.videoWidth/ve.videoHeight
if (aspectRatio > 1){
setDimensions({width: 960, height: iv.height});
}
else if (aspectRatio < 1) {
setDimensions({width: iv.width, height: 960});
}else{
console.log(`aspectRaio: ${aspectRatio}`);
}
}
}
}
return(
<>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Width" value={width} readOnly/>
</div>
<div className="col-md-6">
<Input label="Height" value={height} readOnly/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Frames Cap" value={iv.framesCap} readOnly/>
</div>
<div className="col-md-6">
<Input label="Skip Frames" value={iv.skipFrames} readOnly/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input label="Every n-th Frame" value={iv.everyNthFrame} readOnly />
</div>
<div className="col-md-6">
<Input label="Input FPS" value={iv.inputFps} readOnly/>
</div>
</div>
<div className="row g-3 p-3">
<div className="col-md-6">
<Input
label="Interpolation Multiplier"
value={iv.interpolationMultiplier}
readOnly
/>
</div>
<div className="col-md-6">
</div>
</div>
</>
);
});
@@ -6,7 +6,7 @@ import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/Infer
import { states, Action, State } from "../videoWorkflowReducer";
import { Spinner } from 'components/common';
function StorytellerJobList(){
function VideoWorkflowJobList(){
const failures = (fail = "") => {
switch (fail) {
case "sample case":
@@ -20,12 +20,12 @@ function StorytellerJobList(){
<InferenceJobsList {...{
failures,
onSelect: () => Analytics.voiceConversionClickDownload(),
jobType: FrontendInferenceJobType.StorytellerFilter,
jobType: FrontendInferenceJobType.VideoWorkflow,
}}/>
);
}
export default memo (function PageJobList({
export default memo (function PageWorkflowJoblist({
t, pageState, dispatchPageState
}: {
debug?: boolean;
@@ -37,13 +37,13 @@ export default memo (function PageJobList({
return(
<>
<h1>Jobs</h1>
{pageState.status === states.FILTER_ENQUEUEING &&
{pageState.status === states.WORKFLOW_ENQUEUEING &&
<div>
<h2> Requesting Filter Job</h2>
<Spinner />
</div>
}
<StorytellerJobList />
<VideoWorkflowJobList />
</>
);
});
@@ -2,7 +2,7 @@ import React from "react";
import { NavLink } from "react-router-dom";
import { states, Action, State } from "../videoWorkflowReducer";
import { Button } from "components/common";
import SelectModal from "components/common/SelectModal/SelectModal";
import SelectModal, { SelectModalData } from "components/common/SelectModal/SelectModal";
export default function TabContentLibrary({
t, pageState, dispatchPageState
@@ -14,29 +14,34 @@ export default function TabContentLibrary({
const handleProceed = ()=>{
dispatchPageState({type: "loadFile"})
}
const handleOnSelect = (data:{token:string, title:string})=>{
dispatchPageState({
type: "selectedFile",
payload: {mediaFileToken: data.token}
});
const handleOnSelect = (data:SelectModalData)=>{
if (data.token !== ""){
dispatchPageState({
type: "selectedFile",
payload: {mediaFileToken: data.token}
});
}else{
dispatchPageState({type: "clearedFile"});
};
};
return (
<div className="row g-3">
<div className="col-12">
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
tabs={[
{
label: "All Videos",
tabKey: "allVideos",
mediaTypeFilter: "video",
searcher: false,
type: "media",
},
]}
/>
<SelectModal
modalTitle="Select a Video"
label="Select a Video"
onSelect={handleOnSelect}
tabs={[
{
label: "All Videos",
tabKey: "allVideos",
typeFilter: "video",
searcher: false,
type: "media",
},
]}
/>
</div>
{pageState.status === states.FILE_SELECTED &&
<div className="col-12 d-flex justify-content-center mt-5">
@@ -16,7 +16,7 @@ export default function TabContentUpload({
dispatchPageState: (action: Action) => void;
}) {
const videoProps = useFile({});
const {NO_FILE, FILE_STAGED, FILE_UPLOADING, FILE_UPLOADED} = states;
const {NO_FILE, FILE_STAGED, FILE_SELECTED, FILE_UPLOADING, FILE_UPLOADED} = states;
const makeVideoUploadRequest = () => ({
uuid_idempotency_token: uuidv4(),
@@ -40,7 +40,10 @@ export default function TabContentUpload({
};
// contains upload inout state and controls, see docs
if (pageState.status === NO_FILE || pageState.status === FILE_STAGED) {
if (pageState.status === NO_FILE
|| pageState.status === FILE_STAGED
|| pageState.status === FILE_SELECTED
) {
return (
<>
<div className="row">
@@ -8,8 +8,8 @@ export enum states{
FILE_UPLOADED,
FILE_LOADING,
FILE_LOADED,
FILTER_ENQUEUEING,
FILTER_ENQUEUED
WORKFLOW_ENQUEUEING,
WORKFLOW_ENQUEUED
}
export type State = {
@@ -20,6 +20,7 @@ export type State = {
}
export type Action =
| {type: 'reset'}
| {type: 'stagedFile'}
| {type: 'clearedFile'}
| {type: 'selectedFile', payload: { mediaFileToken: string}}
@@ -67,14 +68,15 @@ export function reducer (state: State, action: Action): State {
case 'enqueueFilter':
return {
...state,
status: states.FILTER_ENQUEUEING
status: states.WORKFLOW_ENQUEUEING
}
case 'enqueueFilterSuccess':
return {
...state,
status: states.FILTER_ENQUEUED,
status: states.WORKFLOW_ENQUEUED,
inferenceJobToken: action.payload.inferenceJobToken
}
case 'reset':
default:
return {status: states.NO_FILE};
@@ -1,9 +1,9 @@
import React from "react";
import {Switch, Route, useRouteMatch, Redirect} from "react-router-dom";
import {Switch, Route, useRouteMatch, Redirect, useHistory} from "react-router-dom";
import PageWorkflowControls from "./components/pageWorkflowControls";
import PageVideoProvision from "./components/pageVideoProvision";
import PageJobList from "./components/pageJobList";
import {State,Action} from "./videoWorkflowReducer";
import PageWorkflowJoblist from "./components/pageWorkflowJoblist";
import {states,State,Action} from "./videoWorkflowReducer";
export default function SubRoutes ({
debug, t, pageState, dispatchPageState
@@ -14,6 +14,15 @@ export default function SubRoutes ({
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){
//reset page if landed on page "Freshly"
dispatchPageState({type: 'reset'})
}
})
const commonPageProps = {
debug, parentPath: path, t, pageState, dispatchPageState
};
@@ -30,7 +39,7 @@ export default function SubRoutes ({
<PageVideoProvision {...commonPageProps}/>
</Route>
<Route exact path={`${path}/jobs`}>
<PageJobList {...commonPageProps}/>
<PageWorkflowJoblist {...commonPageProps}/>
</Route>
<Route path={`${path}`}>
<Redirect to={`${path}/upload`} />
@@ -48,10 +48,11 @@ export default function WeightEditPage({
update,
visibility,
writeStatus,
// status
} = useWeightFetch({ token: weight_token });
const basePath =
weight?.cover_image.maybe_cover_image_public_bucket_path || "";
weight?.cover_image?.maybe_cover_image_public_bucket_path || "";
const currentPath = basePath ? new BucketConfig().getGcsUrl(basePath) : "";
usePrefixedDocumentTitle("Edit Voice");
@@ -63,7 +64,21 @@ export default function WeightEditPage({
let weightToken = weight?.creator?.user_token;
if (!weightToken || !canEditTtsModel(user?.user_token || "")) {
if (isLoading) {
return <Panel padding={true}>
<div className="d-flex flex-column gap-3">
<Skeleton type="short" />
<Skeleton height="40px" />
<Skeleton type="short" />
<Skeleton height="40px" />
<div className="d-flex justify-content-end mt-3 gap-2">
<Skeleton height="40px" width="120px" />
<Skeleton height="40px" width="120px" />
</div>
</div>
</Panel>;
} else {
if (!weightToken) {
return (
<Container type="panel">
<PageHeader
@@ -85,6 +100,28 @@ export default function WeightEditPage({
);
}
if (!canEditTtsModel(user?.user_token || "")) {
return (
<Container type="panel">
<PageHeader
titleIcon={faCircleExclamation}
title="Access Denied"
subText="You do not have permission to edit this model."
panel={true}
extension={
<div className="d-flex">
<Button
label="Back to homepage"
to={`/weight/{}`}
className="d-flex"
/>
</div>
}
/>
</Container>
);
}
return (
<Container type="panel" className="mb-5">
<PageHeader
@@ -96,89 +133,72 @@ export default function WeightEditPage({
backbuttonLabel="Back"
backbuttonTo={`/weight/${weight_token}`}
/>
<>
{isLoading ? (
<Panel padding={true}>
<div className="d-flex flex-column gap-3">
<Skeleton type="short" />
<Skeleton height="40px" />
<Skeleton type="short" />
<Skeleton height="40px" />
<div className="d-flex justify-content-end mt-3 gap-2">
<Skeleton height="40px" width="120px" />
<Skeleton height="40px" width="120px" />
<SplitPanel {...{ busy: writeStatus > 0, dividerFooter: true }}>
<SplitPanel.Body padding={true}>
<div {...{ className: "weight-editor row gy-3 gx-4" }}>
<div {...{ className: "col-12 col-lg-5" }}>
<label className="sub-title">Cover Image</label>
<CoverImageInput
{...{
currentPath,
onClick: coverImg.upload,
status: coverImg.status,
...coverImg.fileProps,
}}
/>
</div>
<div {...{ className: "col-lg-7 order-first order-lg-last" }}>
<TempInput
{...{
label: "Title",
name: "title",
onChange,
placeholder: "Title",
value: title,
}}
/>
<TempSelect
{...{
icon: faEye,
label: "Visibility",
name: "visibility",
options: visibilityOptions,
onChange,
placeholder: "Voice name",
value: visibility,
}}
/>
<TempTextArea
{...{
label: "Description",
name: "descriptionMD",
onChange,
placeholder: "Description",
value: descriptionMD,
}}
/>
</div>
</div>
</Panel>
) : (
<SplitPanel {...{ busy: writeStatus > 0, dividerFooter: true }}>
<SplitPanel.Body padding={true}>
<div {...{ className: "weight-editor row gy-3 gx-4" }}>
<div {...{ className: "col-12 col-lg-5" }}>
<label className="sub-title">Cover Image</label>
<CoverImageInput
{...{
currentPath,
onClick: coverImg.upload,
status: coverImg.status,
...coverImg.fileProps,
}}
/>
</div>
<div {...{ className: "col-lg-7 order-first order-lg-last" }}>
<TempInput
{...{
label: "Title",
name: "title",
onChange,
placeholder: "Title",
value: title,
}}
/>
<TempSelect
{...{
icon: faEye,
label: "Visibility",
name: "visibility",
options: visibilityOptions,
onChange,
placeholder: "Voice name",
value: visibility,
}}
/>
<TempTextArea
{...{
label: "Description",
name: "descriptionMD",
onChange,
placeholder: "Description",
value: descriptionMD,
}}
/>
</div>
</div>
</SplitPanel.Body>
<SplitPanel.Footer padding={true}>
<div className="d-flex gap-2 justify-content-end">
<Button
{...{
label: "Cancel",
to: `/weight/${weight_token}`,
variant: "secondary",
}}
/>
<Button
{...{
label: "Save Changes",
onClick: update,
}}
/>
</div>
</SplitPanel.Footer>
</SplitPanel>
)}
</>
</SplitPanel.Body>
<SplitPanel.Footer padding={true}>
<div className="d-flex gap-2 justify-content-end">
<Button
{...{
label: "Cancel",
to: `/weight/${weight_token}`,
variant: "secondary",
}}
/>
<Button
{...{
label: "Save Changes",
onClick: update,
}}
/>
</div>
</SplitPanel.Footer>
</SplitPanel>
</Container>
);
}
}
@@ -35,8 +35,11 @@ import WeightCoverImage from "components/common/WeightCoverImage";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import SdInferencePanel from "./inference_panels/SdInferencePanel";
import SdCoverImagePanel from "./cover_image_panels/SdCoverImagePanel";
import { StudioNotAvailable } from "v2/view/_common/StudioNotAvailable";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
interface WeightProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
inferenceJobs: Array<InferenceJob>;
ttsInferenceJobs: Array<TtsInferenceJob>;
@@ -49,6 +52,7 @@ interface WeightProps {
}
export default function WeightPage({
sessionWrapper,
sessionSubscriptionsWrapper,
inferenceJobs,
ttsInferenceJobs,
@@ -100,6 +104,15 @@ export default function WeightPage({
const deleteWeight = () => remove(!!user?.can_ban_users);
switch (weight?.weight_type) {
case WeightType.SD_15:
case WeightType.SDXL:
case WeightType.LORA:
if (!sessionWrapper.canAccessStudio()) {
return <StudioNotAvailable />
}
}
function renderWeightComponent(weight: Weight) {
switch (weight.weight_category) {
case WeightCategory.TTS:
@@ -299,14 +299,14 @@ function SdInferencePanel({
{
label: "All Weights",
tabKey: "allWeights",
weightTypeFilter: "sd_1.5",
typeFilter: "sd_1.5",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedWeights",
weightTypeFilter: "sd_1.5",
typeFilter: "sd_1.5",
searcher: false,
type: "weights",
},
@@ -409,14 +409,14 @@ function SdInferencePanel({
{
label: "All LoRA Weights",
tabKey: "allLoraWeights",
weightTypeFilter: "rvc_v2",
typeFilter: "rvc_v2",
searcher: true,
type: "weights",
},
{
label: "Bookmarked",
tabKey: "bookmarkedLoraWeights",
weightTypeFilter: "rvc_v2",
typeFilter: "rvc_v2",
searcher: false,
type: "weights",
},