Continue - Inference Job for Pre-loading User Media (#434)

- Raised these signals for background loaded media to the app level
- Raised the enums and types for these signals to the app level
- Delete the old useInferenceManager that only specifically handles
audio inferences
- Use the new signal and its implementation pattern to handle audio
inferences
- Clean up and simplify the implementation pattern to this new signal
- Variable polling frequencies depending what kind of active jobs exist
in the list

---------

Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
wilandwil authored and GitHub committed 2024-05-29 18:08:03 -04:00
1 parent a4e8e68b8b
commit 65b91ce54e
30 files changed
+423 -563

No files matched your search

@@ -9,7 +9,9 @@ import { activeWorkflowJobs } from "~/signals";
export const MyMoviesButton = () => {
useSignals();
const activeCount = activeWorkflowJobs.value.length;
const activeCount = activeWorkflowJobs.value
? activeWorkflowJobs.value.length
: 0;
return (
<div className="relative">
+1
View File
@@ -6,3 +6,4 @@ export const ONE_MINUTE = 60000;
export const TWO_SECONDS = 2000;
export const FIVE_SECONDS = 5000;
export const SEVEN_SECONDS = 7000;
export const TEN_SECONDS = 10000;
+6
View File
@@ -8,3 +8,9 @@ export enum JobStatus {
CANCELLED_BY_USER = "cancelled_by_user",
CANCCELLED_BY_SYSTEM = "cancelled_by_system",
}
export enum JobType {
TextToSpeech = "text_to_speech",
VoiceConversion = "voice_conversion",
VideoStyleTransfer = "workflow",
}
+1 -1
View File
@@ -1 +1 @@
export * from "./useInferenceJobManager";
export * from "./useActiveJobs";
+1
View File
@@ -0,0 +1 @@
export { useActiveJobs } from "./useActiveJobs";
@@ -1,5 +1,9 @@
import { Job } from "~/models";
export type WindowExtended = typeof window & {
stopPollingActiveJobs: () => void;
};
export type GetJobsResponse = {
success: boolean;
error_message?: string;
+130
View File
@@ -0,0 +1,130 @@
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { JobType } from "~/enums";
import {
activeJobs,
shouldPollActiveJobs,
startPollingActiveJobs,
stopPollingActiveJobs,
} from "~/signals";
import { PollRecentJobs } from "./utilities";
import { TEN_SECONDS, TWO_SECONDS } from "~/constants";
import { WindowExtended } from "./types";
export const useActiveJobs = () => {
useSignals();
const [timerState, setTimerState] = useState<{
timer: NodeJS.Timeout | undefined;
interval: number;
}>({
timer: undefined,
interval: TWO_SECONDS,
});
const resetTimer = useCallback((newTimeInterval: number) => {
setTimerState((curr) => {
if (!curr.timer) {
// if no timer, just change interval
return {
timer: curr.timer,
interval: newTimeInterval,
};
}
// if timer already exist, first clear
// then set new one with new interval
clearInterval(curr.timer);
const newTimer = setInterval(() => {
PollRecentJobs();
}, newTimeInterval);
return {
timer: newTimer,
interval: newTimeInterval,
};
});
}, []);
const clearTimer = useCallback(() => {
if (timerState.timer) {
clearInterval(timerState.timer);
setTimerState((curr) => ({
...curr,
timer: undefined,
}));
}
}, [timerState.timer]);
useEffect(() => {
return () => {
//make sure we run clear timer as well on dismount;
clearInterval(timerState.timer);
};
}, [timerState.timer]);
useLayoutEffect(() => {
//TODO: add dev account guard
(window as WindowExtended).stopPollingActiveJobs = stopPollingActiveJobs;
}, []);
useSignalEffect(() => {
// if active jobs is not initiated, do do a first pull;
if (!activeJobs.value) {
PollRecentJobs();
return;
}
// if there are no active jobs, stop making polls
if (activeJobs.value.length === 0) {
stopPollingActiveJobs();
return;
}
if (activeJobs.value.length > 0) {
// if there are active jobs, first, manage polling interval
const hasShortJobs =
activeJobs.value.find((job) => {
return (
job.request.inference_category === JobType.TextToSpeech ||
job.request.inference_category === JobType.VoiceConversion
);
}) !== undefined;
if (!hasShortJobs && timerState.interval < TEN_SECONDS) {
resetTimer(TEN_SECONDS);
}
if (hasShortJobs && timerState.interval > TWO_SECONDS) {
resetTimer(TWO_SECONDS);
}
// if there are active jobs, and is not polling, start polling
if (!shouldPollActiveJobs.value) {
startPollingActiveJobs();
}
}
});
useSignalEffect(() => {
//trigger the pull via a timer
if (shouldPollActiveJobs.value) {
setTimerState((curr) => {
if (curr.timer) {
//do nothing if timer is already running;
return curr;
}
//else make and set timer
const newTimer = setInterval(() => {
PollRecentJobs();
}, curr.interval);
return {
...curr,
timer: newTimer,
};
});
return;
}
// should not poll, then clear timer
clearTimer();
});
};
+27
View File
@@ -0,0 +1,27 @@
import { getRecentJobs } from "~/api";
import { ToastTypes } from "~/enums";
import { authentication, addToast, setJobs } from "~/signals";
export function PollRecentJobs() {
console.log("Poll recent jobs");
const endpoint = getRecentJobs;
fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
session: authentication.sessionToken.value || "",
},
credentials: "include",
})
.then((res) => res.json())
.then((res) => {
if (res.jobs && res.jobs.length > 0) {
setJobs(res.jobs);
// just no jobs, not an error, do nothing
}
})
.catch(() => {
addToast(ToastTypes.ERROR, "Unknown Error in Getting Recent Jobs");
});
}
@@ -1,47 +1,18 @@
import { useCallback } from "react";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { GetUserMovies, instanceOfMediaListResponse } from "./utilities";
import { addToast, authentication, completedJobs } from "~/signals";
import { PollUserMovies, PollUserAudioItems } from "./utilities";
import {
myMovies,
setMyMovies,
shouldPollMyMovies,
} from "~/pages/PageEnigma/signals";
import { ToastTypes } from "~/enums";
import { completedJobs, userMovies, userAudioItems } from "~/signals";
import { JobType } from "~/enums";
export const useBackgroundLoadingMedia = () => {
useSignals();
const PollMyMovies = useCallback(() => {
// console.log("polling my movies");
if (authentication.userInfo.value?.username) {
GetUserMovies(authentication.userInfo.value.username).then((res) => {
// console.log("GetUerMovies has response:", res);
if (instanceOfMediaListResponse(res)) {
setMyMovies(res.results);
} else {
addToast(ToastTypes.ERROR, res.error_reason);
}
});
}
}, []);
useSignalEffect(() => {
if (shouldPollMyMovies.value) {
// shouldPollMyMovies is initiated as true to guaruntee first pull on load
PollMyMovies();
}
});
useSignalEffect(() => {
//CASE 1: first load
// if myMovies undefined, first load is not done
if (!myMovies.value) {
// turn on polling if it is not already on
if (!shouldPollMyMovies.value) {
shouldPollMyMovies.value = true;
}
// if myMovies undefined, poll for the first time
if (!userMovies.value) {
PollUserMovies();
return;
}
@@ -53,7 +24,7 @@ export const useBackgroundLoadingMedia = () => {
const workflowJobsTokens = completedJobs.value
.filter((job) => {
if (
job.request.inference_category === "workflow" &&
job.request.inference_category === JobType.VideoStyleTransfer &&
job.maybe_result.entity_token
) {
return true;
@@ -67,21 +38,61 @@ export const useBackgroundLoadingMedia = () => {
return;
}
const myMoviesTokens = myMovies.value.map((movie) => movie.token);
const userMoviesTokens = userMovies.value.map((movie) => movie.token);
const isEveryTokenIncludedInPolled = workflowJobsTokens.every((token) =>
myMoviesTokens.includes(token),
userMoviesTokens.includes(token),
);
if (!isEveryTokenIncludedInPolled) {
//there are videos newly completed, poll
shouldPollMyMovies.value = true;
PollUserMovies();
return;
}
// at this point, make sure we stop polling because:
// - myMovies first load is done
// - completed jobs that contain new videos are all polled
if (shouldPollMyMovies.value) {
shouldPollMyMovies.value = false;
//else, no new movies, no need to poll;
});
useSignalEffect(() => {
//CASE 1: first load
// if audioItems undefined, poll for the first time
if (!userAudioItems.value) {
PollUserAudioItems();
return;
}
//CASE 2: pull after jobs completion
if (!completedJobs.value || completedJobs.value.length === 0) {
return; // nothing to do if there's no complete jobs
}
const audioJobsTokens = completedJobs.value
.filter((job) => {
if (
(job.request.inference_category === JobType.TextToSpeech ||
job.request.inference_category === JobType.VoiceConversion) &&
job.maybe_result.entity_token
) {
return true;
}
return false;
})
.map((job) => job.maybe_result.entity_token);
if (audioJobsTokens.length === 0) {
//nothing to do if no complete jobs involves audio
return;
}
const userAudioItemsTokens = userAudioItems.value.map(
(audio) => audio.media_id,
);
const isEveryTokenIncludedInPolled = audioJobsTokens.every((token) =>
userAudioItemsTokens.includes(token),
);
if (!isEveryTokenIncludedInPolled) {
//there are videos newly completed, poll
PollUserAudioItems();
return;
}
//else, no new movies, no need to poll;
});
};
@@ -1,30 +1,56 @@
import { MediaFileType } from "~/pages/PageEnigma/enums";
import { ToastTypes } from "~/enums";
import {
GetMediaByUser,
GetMediaListResponse,
} from "~/api/media_files/GetMediaByUser";
export function instanceOfMediaListResponse(
object: GetMediaListResponse | { success: boolean; error_reason: string },
): object is GetMediaListResponse {
return "results" in object;
}
export function GetUserMovies(username: string) {
import {
addToast,
authentication,
setUserAudioItems,
setUserMovies,
} from "~/signals";
const { userInfo } = authentication;
export function PollUserMovies() {
if (!userInfo.value) {
//do nothing return if login info does not exist
return;
}
return GetMediaByUser(
username,
userInfo.value.username,
{},
{
filter_media_type: MediaFileType.Video,
},
)
.then((res: GetMediaListResponse) => {
return res;
setUserMovies(res.results);
})
.catch(() => {
return {
success: false,
error_reason: "Unknown Error in Loading My Movies",
};
addToast(ToastTypes.ERROR, "Unknown Error in Loading My Movies");
});
}
export function PollUserAudioItems() {
if (!userInfo.value) {
//do nothing return if login info does not exist
return;
}
return GetMediaByUser(
userInfo.value.username,
{},
{
filter_media_type: MediaFileType.Audio,
},
)
.then((res: GetMediaListResponse) => {
setUserAudioItems(res.results);
})
.catch(() => {
addToast(ToastTypes.ERROR, "Unknown Error in Loading My Audio Items");
});
}
@@ -1,2 +0,0 @@
export { useInferenceJobManager } from "./useInferenceJobManager";
export { useActiveJobs } from "./useActiveJobs";
@@ -1,68 +0,0 @@
import { useEffect } from "react";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { GetJobsResponse } from "./types";
import { ToastTypes } from "~/enums";
import { SEVEN_SECONDS } from "~/constants";
import {
activeJobs,
addToast,
setJobs,
shouldPollActiveJobs,
startPollingActiveJobs,
stopPollingActiveJobs,
} from "~/signals";
import { GetRecentJobs } from "./utilities";
export const useActiveJobs = () => {
useSignals();
useEffect(() => {
const PollRecentJobs = () => {
GetRecentJobs().then((res: GetJobsResponse) => {
if (!res.success) {
addToast(
ToastTypes.ERROR,
res.error_message || "Unknown Error in Getting Recent Jobs",
);
}
if (res.jobs && res.jobs.length > 0) {
setJobs(res.jobs);
}
// just no jobs, not an error, do nothing
});
};
// Initial Poll
PollRecentJobs();
//trigger the pull every two seconds
const intervalTimer = setInterval(() => {
if (shouldPollActiveJobs.value) {
//but only do it if necessary
PollRecentJobs();
}
}, SEVEN_SECONDS);
return () => {
clearInterval(intervalTimer);
};
}, []);
useSignalEffect(() => {
// if active jobs is not initiated, do nothing
if (!activeJobs.value) {
return;
}
// if there are no active jobs, stop making polls
if (activeJobs.value.length === 0) {
stopPollingActiveJobs();
return;
}
// if there are active jobs, and is not polling, start polling
if (activeJobs.value.length > 0 && !shouldPollActiveJobs.value) {
startPollingActiveJobs();
}
});
};
@@ -1,55 +0,0 @@
import { useCallback, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { ErrorResponse, GetJobStatusResponse } from "~/pages/PageEnigma/models";
import { JobState } from "~/pages/PageEnigma/enums";
import { ToastTypes } from "~/enums";
import { addToast } from "~/signals";
import {
inferenceJobs,
updateInferenceJob,
} from "~/pages/PageEnigma/signals/inferenceJobs";
import { GetInferenceJobStatus } from "./utilities";
function shouldKeepPolling(currStatus: string) {
return currStatus === JobState.PENDING || currStatus === JobState.STARTED;
}
export const useInferenceJobManager = () => {
useSignals();
const pollInferenceJobs = useCallback(() => {
if (inferenceJobs.value.length > 0) {
inferenceJobs.value.forEach((job) => {
if (shouldKeepPolling(job.job_status)) {
GetInferenceJobStatus(job.job_id).then(
(res: GetJobStatusResponse | ErrorResponse) => {
if (res?.success) {
console.log("inference", res);
const response = res as GetJobStatusResponse;
updateInferenceJob({
...job,
job_status: response.state.status.status,
result: response.state.maybe_result,
});
return;
}
addToast(ToastTypes.ERROR, (res as ErrorResponse).error_reason);
},
);
}
});
}
}, []);
useEffect(() => {
const intervalTimer = setInterval(() => {
pollInferenceJobs();
}, 2000);
return () => {
clearInterval(intervalTimer);
};
}, [pollInferenceJobs]);
};
@@ -1,76 +0,0 @@
import { ErrorResponse, GetJobStatusResponse } from "~/pages/PageEnigma/models";
import { getActiveJobs, getJobStatus, getRecentJobs } from "~/api";
import { authentication } from "~/signals";
export function GetInferenceJobStatus(
jobToken: string,
): Promise<GetJobStatusResponse | ErrorResponse> {
const endpoint = getJobStatus(jobToken);
return fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
session: authentication.sessionToken.value || "",
},
credentials: "include",
})
.then((res) => res.json())
.then((res) => {
const response: GetJobStatusResponse | ErrorResponse = res;
return response;
})
.catch(() => {
return {
success: false,
error_reason: "Unknown error",
};
});
}
export function GetRecentJobs() {
const endpoint = getRecentJobs;
return fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
session: authentication.sessionToken.value || "",
},
credentials: "include",
})
.then((res) => res.json())
.then((res) => {
return res;
})
.catch(() => {
return {
success: false,
error_reason: "Unknown Error in Getting Recent Jobs",
};
});
}
export function GetActiveJobs() {
const endpoint = getActiveJobs;
return fetch(endpoint, {
method: "GET",
headers: {
Accept: "application/json",
session: authentication.sessionToken.value || "",
},
credentials: "include",
})
.then((res) => res.json())
.then((res) => {
return res;
})
.catch(() => {
return {
success: false,
error_reason: "Unknown error",
};
});
}
+1 -2
View File
@@ -2,14 +2,13 @@ import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackProvider";
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
import { EngineProvider } from "./contexts/EngineContext";
import { useInferenceJobManager, useActiveJobs } from "~/hooks";
import { useActiveJobs } from "~/hooks";
import { useQueueHandler } from "./hooks/useQueueHandler";
import { ErrorDialog, LoadingBar } from "~/components";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { useBackgroundLoadingMedia } from "~/hooks/useBackgroundLoadingMedia";
export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
useInferenceJobManager();
useActiveJobs();
useBackgroundLoadingMedia();
@@ -1,15 +1,12 @@
import {
// generateMovieId,
myMovies,
viewMyMovies,
} from "~/pages/PageEnigma/signals";
import { viewMyMovies } from "~/pages/PageEnigma/signals";
import { CompletedCard } from "~/pages/PageEnigma/comps/GenerateModals/CompletedCard";
import { InProgressCard } from "~/pages/PageEnigma/comps/GenerateModals/InProgressCard";
import { useSignals } from "@preact/signals-react/runtime";
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
import { TransitionDialogue } from "~/components";
import { activeWorkflowJobs } from "~/signals";
import { activeWorkflowJobs, userMovies } from "~/signals";
interface Props {
setMovieId: (page: string) => void;
}
@@ -41,7 +38,7 @@ export function MyMovies({ setMovieId }: Props) {
<div>
<div className="mx-5 mb-1 font-medium">Completed</div>
<div className="flex flex-col">
{myMovies.value?.map((movie) => (
{userMovies.value?.map((movie) => (
<CompletedCard
key={movie.token}
movie={movie}
@@ -1,40 +1,21 @@
import { useCallback, useEffect, useState } from "react";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useSignals } from "@preact/signals-react/runtime";
import { AssetType } from "~/enums";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
import { AudioPanelState } from "~/pages/PageEnigma/models/voice";
import {
AudioTabPages,
InferenceJobType,
JobState,
} from "~/pages/PageEnigma/enums";
import { AudioPanelState } from "../../../../models/voice";
import {
AudioMediaItem,
MediaInfo,
TtsModelListItem,
VoiceConversionModelListItem,
} from "~/pages/PageEnigma/models";
import {
addInferenceJob,
inferenceJobs,
} from "~/pages/PageEnigma/signals/inferenceJobs";
import {
audioItemsFromServer,
isRetreivingAudioItems,
} from "~/pages/PageEnigma/signals/mediaFromServer";
import {
ListAudioByUser,
ListTtsModels,
ListVoiceConversionModels,
} from "./utilities";
import { ListTtsModels, ListVoiceConversionModels } from "./utilities";
import { initialTtsState, initialV2VState } from "./values";
import { PageLibrary } from "./pageLibrary";
import { PageAudioLibrary } from "./pageAudioLibrary";
import { PageAudioGeneration } from "./pageAudioGeneration";
import { PageSelectTtsModel } from "./pageSelectTtsModel";
import { PageSelectV2VModel } from "./pageSelectV2VModel";
import { PollUserAudioItems } from "~/hooks/useBackgroundLoadingMedia/utilities";
export const AudioTab = () => {
useSignals();
@@ -54,71 +35,9 @@ export const AudioTab = () => {
Array<VoiceConversionModelListItem>
>([]);
const handleListAudioByUser = useCallback(() => {
function getTitle(item: MediaInfo) {
// console.log(item);
if (item.maybe_title) return item.maybe_title;
if (
item.origin &&
item.origin.maybe_model &&
item.origin.maybe_model.title
)
return item.origin.maybe_model.title;
return "Media Audio";
}
function getCategory(item: MediaInfo) {
if (
item.origin &&
item.origin.product_category &&
item.origin.product_category !== "unknown"
)
return item.origin.product_category;
if (item.origin_category) return item.origin_category;
return "unknown";
}
function checkIsNew(previousItems: AudioMediaItem[], token: string) {
const foundItem = previousItems.find((item) => {
return token === item.media_id && item.isNew !== true;
});
return foundItem === undefined;
}
function getLength(item: MediaInfo) {
return item.maybe_duration_millis
? (item.maybe_duration_millis / 1000) * 60
: undefined;
}
isRetreivingAudioItems.value = true;
ListAudioByUser().then((res: MediaInfo[]) => {
isRetreivingAudioItems.value = false;
const previousItems = [...audioItemsFromServer.value];
audioItemsFromServer.value = res.map((item) => {
const morphedItem: AudioMediaItem = {
version: 1,
type: AssetType.AUDIO,
category: getCategory(item),
media_id: item.token,
object_uuid: item.token,
name: getTitle(item),
description: item.maybe_text_transcript || "",
publicBucketPath: item.public_bucket_path,
length: getLength(item),
thumbnail: "/resources/placeholders/audio_placeholder.png",
isMine: true,
isNew:
previousItems.length > 0
? checkIsNew(previousItems, item.token)
: false,
// isBookmarked?: boolean;
};
return morphedItem;
});
});
}, []);
useEffect(() => {
if (!state.firstLoad) {
//fetch all the data on first load once, after securing auth info
handleListAudioByUser();
ListTtsModels().then((res) => {
if (res) setTtsModels(res);
});
@@ -128,79 +47,7 @@ export const AudioTab = () => {
setState((curr) => ({ ...curr, firstLoad: true }));
// completed the first load
}
}, [state, handleListAudioByUser]);
useEffect(() => {
//this listens to ttsState and push its new inference jobs
setState((curr) => {
const { ttsState: currTtsState } = curr;
if (currTtsState.hasEnqueued < currTtsState.inferenceTokens.length) {
addInferenceJob({
version: 1,
job_id:
currTtsState.inferenceTokens[
currTtsState.inferenceTokens.length - 1
],
job_type: InferenceJobType.TextToSpeech,
job_status: JobState.PENDING,
});
return {
...curr,
ttsState: {
...currTtsState,
hasEnqueued: currTtsState.hasEnqueued + 1,
},
};
}
return curr; //case of no new jobs, do nothing
});
}, [state.ttsState]);
useEffect(() => {
//this listens to v2vState and push its new inference jobs
setState((curr) => {
const { v2vState: currV2VState } = curr;
if (currV2VState.hasEnqueued < currV2VState.inferenceTokens.length) {
addInferenceJob({
version: 1,
job_id:
currV2VState.inferenceTokens[
currV2VState.inferenceTokens.length - 1
],
job_type: InferenceJobType.VoiceConversion,
job_status: JobState.PENDING,
});
return {
...curr,
v2vState: {
...currV2VState,
hasEnqueued: currV2VState.hasEnqueued + 1,
},
};
}
return curr; //case of no new jobs, do nothing
});
}, [state.v2vState]);
useSignalEffect(() => {
// when inference changes, check if there's a new audio to refresh for
let hasNewCompletedJob = false;
inferenceJobs.value.forEach((job) => {
if (
job.job_status === JobState.COMPLETE_SUCCESS &&
(job.job_type === InferenceJobType.TextToSpeech ||
job.job_type === InferenceJobType.VoiceConversion)
) {
const foundItemOfJob = audioItemsFromServer.value.find((item) => {
return item.media_id === job.result.entity_token;
});
hasNewCompletedJob = foundItemOfJob === undefined;
}
});
if (hasNewCompletedJob) {
handleListAudioByUser();
}
});
}, [state]);
const changePage = useCallback((newPage: AudioTabPages) => {
setState((curr) => ({
@@ -212,9 +59,9 @@ export const AudioTab = () => {
switch (state.page) {
case AudioTabPages.LIBRARY: {
return (
<PageLibrary
<PageAudioLibrary
changePage={changePage}
reloadLibrary={handleListAudioByUser}
reloadLibrary={PollUserAudioItems}
/>
);
}
@@ -6,33 +6,33 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { InferenceJob } from "~/pages/PageEnigma/models";
import { JobStatus } from "~/enums";
import { Job } from "~/models";
import { ButtonIcon, H5 } from "~/components";
import { JobState } from "~/pages/PageEnigma/enums";
export const InferenceElement = ({ job }: { job: InferenceJob }) => {
export const InferenceElement = ({ job }: { job: Job }) => {
const className = twMerge(
"rounded-md w-full flex justify-between items-center p-2 gap-2",
job.job_status === JobState.PENDING ? "bg-inference-pending" : "",
job.job_status === JobState.STARTED ? "bg-inference-generating" : "",
job.job_status === JobState.DEAD ? "bg-inference-error" : "",
job.status.status === JobStatus.PENDING ? "bg-inference-pending" : "",
job.status.status === JobStatus.STARTED ? "bg-inference-generating" : "",
job.status.status === JobStatus.DEAD ? "bg-inference-error" : "",
);
const statusText = "".concat(
job.job_status === JobState.PENDING ? "Pending..." : "",
job.job_status === JobState.STARTED ? "Generating..." : "",
job.job_status === JobState.DEAD ? "Error" : "",
job.status.status === JobStatus.PENDING ? "Pending..." : "",
job.status.status === JobStatus.STARTED ? "Generating..." : "",
job.status.status === JobStatus.DEAD ? "Error" : "",
);
return (
<div className={className}>
{job.job_status === JobState.DEAD && (
{job.status.status === JobStatus.DEAD && (
<FontAwesomeIcon icon={faCircleXmark} />
)}
{job.job_status !== JobState.DEAD && (
{job.status.status !== JobStatus.DEAD && (
<FontAwesomeIcon icon={faSpinnerThird} spin />
)}
<H5 className="grow">{statusText}</H5>
{job.job_status === JobState.DEAD && (
{job.status.status === JobStatus.DEAD && (
<ButtonIcon
icon={faXmark}
onClick={() => {
@@ -3,8 +3,6 @@ import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { useSignals, useComputed } from "@preact/signals-react/runtime";
import { audioFilter, audioItems } from "~/pages/PageEnigma/signals";
import { audioItemsFromServer } from "~/pages/PageEnigma/signals/mediaFromServer";
import { inferenceJobs } from "~/pages/PageEnigma/signals/inferenceJobs";
import { Button, Pagination, UploadAudioButtonDialogue } from "~/components";
@@ -12,13 +10,11 @@ import { AudioItemElements } from "./audioItemElements";
import { TabTitle } from "~/pages/PageEnigma/comps/SidePanelTabs/comps/TabTitle";
import { InferenceElement } from "./inferenceElement";
import { AssetFilterOption } from "~/enums";
import {
AudioTabPages,
InferenceJobType,
JobState,
} from "~/pages/PageEnigma/enums";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
export const PageLibrary = ({
import { activeAudioJobs, userAudioItems } from "~/signals";
export const PageAudioLibrary = ({
changePage,
reloadLibrary,
}: {
@@ -26,9 +22,10 @@ export const PageLibrary = ({
reloadLibrary: () => void;
}) => {
useSignals();
const loadUserAudioItems = userAudioItems.value ? userAudioItems.value : [];
const allAudioItems = useComputed(() => [
...audioItems.value,
...audioItemsFromServer.value,
...loadUserAudioItems,
]);
const displayedItems = allAudioItems.value.filter((item) => {
if (audioFilter.value === AssetFilterOption.ALL) {
@@ -39,17 +36,7 @@ export const PageLibrary = ({
}
return item.isBookmarked;
});
const audioInferenceJobs = useComputed(() =>
inferenceJobs.value.filter((job) => {
if (
job.job_status !== JobState.COMPLETE_SUCCESS &&
(job.job_type === InferenceJobType.TextToSpeech ||
job.job_type === InferenceJobType.VoiceConversion)
) {
return job;
}
}),
);
const pageSize = 20;
const totalPages = Math.ceil(allAudioItems.value.length / pageSize);
const [currentPage, setCurrentPage] = useState<number>(0);
@@ -109,10 +96,10 @@ export const PageLibrary = ({
</div>
<div className="w-full grow overflow-y-auto px-4">
{audioInferenceJobs.value.length > 0 && (
{activeAudioJobs.value && activeAudioJobs.value.length > 0 && (
<div className="mb-4 grid grid-cols-1 gap-2">
{audioInferenceJobs.value.map((job) => {
return <InferenceElement key={job.job_id} job={job} />;
{activeAudioJobs.value.map((job) => {
return <InferenceElement key={job.job_token} job={job} />;
})}
</div>
)}
@@ -11,6 +11,7 @@ import { GenerateTtsAudioResponse } from "~/pages/PageEnigma/models/tts";
import { GenerateTtsAudio } from "./utilities";
import { TtsState } from "../../../../models/voice";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
import { startPollingActiveJobs } from "~/signals";
export const PageTTS = ({
changePage,
@@ -33,21 +34,14 @@ export const PageTTS = ({
GenerateTtsAudio(request).then((res: GenerateTtsAudioResponse) => {
if (res && res.inference_job_token) {
setTtsState({
...ttsState,
inferenceTokens: [
...ttsState.inferenceTokens,
res.inference_job_token,
],
});
startPollingActiveJobs();
changePage(AudioTabPages.LIBRARY);
}
});
} else {
console.log("no voice model selected");
}
}, [ttsState, changePage, setTtsState]);
}, [ttsState, changePage]);
const handleTextInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTtsState({
@@ -13,6 +13,7 @@ import { GenerateVoiceConversion } from "./utilities";
import { H4, H6, Button, Label, UploadAudioComponent } from "~/components";
import { AudioTabPages } from "~/pages/PageEnigma/enums";
import { startPollingActiveJobs } from "~/signals";
export const PageVoicetoVoice = ({
changePage,
@@ -35,19 +36,14 @@ export const PageVoicetoVoice = ({
GenerateVoiceConversion(request).then(
(res: EnqueueVoiceConversionResponse) => {
if (res && res.inference_job_token) {
setV2VState({
...v2vState,
inferenceTokens: [
...v2vState.inferenceTokens,
res.inference_job_token,
],
});
startPollingActiveJobs();
changePage(AudioTabPages.LIBRARY);
}
},
);
}
}, [v2vState, changePage, setV2VState]);
}, [v2vState, changePage]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col">
@@ -3,14 +3,10 @@ import { TtsState, V2VState } from "../../../../models/voice";
export const initialTtsState: TtsState = {
voice: undefined,
text: "",
hasEnqueued: 0,
inferenceTokens: [],
};
export const initialV2VState: V2VState = {
voice: undefined,
file: undefined,
inputFileToken: undefined,
hasEnqueued: 0,
inferenceTokens: [],
};
@@ -1,13 +0,0 @@
export enum InferenceJobType {
FaceAnimation,
TextToSpeech,
VoiceConversion,
VoiceDesignerCreateVoice,
VoiceDesignerTts,
ImageGeneration,
VideoMotionCapture,
ConvertFbxtoGltf,
EngineComposition,
VideoWorkflow,
VideoStyleTransfer,
}
-1
View File
@@ -5,7 +5,6 @@ export * from "./ClipType";
export * from "./EditorState";
export * from "./FetchStatus";
export * from "./GenerateTtsAudioErrorType";
export * from "./InferenceJobType";
export * from "./JobState";
export * from "./MediaFileClass";
export * from "./MediaFileType";
-4
View File
@@ -12,14 +12,10 @@ export type AudioPanelState = {
export type TtsState = {
voice: TtsModelListItem | undefined;
text: string;
hasEnqueued: number;
inferenceTokens: string[];
};
export type V2VState = {
voice: VoiceConversionModelListItem | undefined;
file: File | undefined;
inputFileToken: string | undefined;
hasEnqueued: number;
inferenceTokens: string[];
};
@@ -1,42 +0,0 @@
import { signal } from "@preact/signals-core";
import { InferenceJob } from "../models";
export const inferenceJobs = signal<InferenceJob[]>([]);
export function addInferenceJob(newJob: InferenceJob) {
const existingInferenceJobs = inferenceJobs.value;
const jobExist = existingInferenceJobs.find(
(job) => job.job_id == newJob.job_id,
);
if (!jobExist) {
inferenceJobs.value = [...existingInferenceJobs, newJob];
}
// else do nothing
}
export function updateInferenceJob(updatableJob: InferenceJob) {
const existingInferenceJobs = inferenceJobs.value;
existingInferenceJobs.forEach((job) => {
if (job.job_id === updatableJob.job_id) {
job.job_status = updatableJob.job_status;
}
if (updatableJob.result) {
job.result = updatableJob.result;
}
});
inferenceJobs.value = [...existingInferenceJobs];
}
export function deleteInferenceJob(deletableJob: InferenceJob) {
// console.log("delete");
const newList: InferenceJob[] = [];
inferenceJobs.value.forEach((job) => {
if (job.job_id !== deletableJob.job_id) {
newList.push(job);
}
// or else do nothing
});
// console.log(newList);
inferenceJobs.value = [...newList];
}
-10
View File
@@ -1,14 +1,4 @@
import { signal } from "@preact/signals-core";
import { MediaInfo } from "~/pages/PageEnigma/models/movies";
export const viewMyMovies = signal(false);
export const generateMovieId = signal("");
export const myMovies = signal<MediaInfo[] | undefined>(undefined);
export const setMyMovies = (newSet: MediaInfo[]) => {
myMovies.value = newSet;
};
export const shouldPollMyMovies = signal<boolean>(true);
+20 -6
View File
@@ -1,10 +1,11 @@
import { signal, computed } from "@preact/signals-core";
import { JobStatus } from "~/enums";
import { JobStatus, JobType } from "~/enums";
import { PollRecentJobs } from "~/hooks/useActiveJobs/utilities";
import { Job } from "~/models";
import { isJobStatusTerminal } from "~/utilities";
export const recentJobs = signal<Job[] | undefined>([]);
export const recentJobs = signal<Job[] | undefined>(undefined);
export const setJobs = (newJobList: Job[]) => {
recentJobs.value = [...newJobList];
};
@@ -24,9 +25,22 @@ export const activeWorkflowJobs = computed(() => {
return undefined;
}
return activeJobs.value.filter((job) => {
return job.request.inference_category === "workflow";
return job.request.inference_category === JobType.VideoStyleTransfer;
});
});
export const activeAudioJobs = computed(() => {
if (!activeJobs.value) {
return undefined;
}
return activeJobs.value.filter((job) => {
return (
job.request.inference_category === JobType.TextToSpeech ||
job.request.inference_category === JobType.VoiceConversion
);
});
});
export const completedJobs = computed(() => {
if (!recentJobs.value) {
return undefined;
@@ -36,13 +50,13 @@ export const completedJobs = computed(() => {
});
});
export const shouldPollActiveJobs = signal<boolean>(true);
export const shouldPollActiveJobs = signal<boolean>(false);
export const startPollingActiveJobs = () => {
console.info("start polling Active Jobs");
PollRecentJobs(); //poll once regardless of polling intervals
shouldPollActiveJobs.value = true;
};
export const stopPollingActiveJobs = () => {
console.info("stop polling Active Jobs");
shouldPollActiveJobs.value = false;
};
+92
View File
@@ -0,0 +1,92 @@
import { signal } from "@preact/signals-core";
import { AssetType } from "~/enums";
import { MediaInfo } from "~/pages/PageEnigma/models/movies";
import { AudioMediaItem } from "~/pages/PageEnigma/models";
// Signal for background loading User's Movies
export const userMovies = signal<MediaInfo[] | undefined>(undefined);
export const setUserMovies = (newSet: MediaInfo[]) => {
userMovies.value = newSet;
};
//Signal for background loading User's Audio Items.
export const userAudioItems = signal<AudioMediaItem[] | undefined>(undefined);
export const setUserAudioItems = (newSet: MediaInfo[]) => {
const previousItems = userAudioItems.value ? [...userAudioItems.value] : [];
const morphedNewSet = newSet.map((item) => {
const morphedItem: AudioMediaItem = {
version: 1,
type: AssetType.AUDIO,
category: getCategory(item),
media_id: item.token,
object_uuid: item.token,
name: getTitle(item),
description: item.maybe_text_transcript || "",
publicBucketPath: item.public_bucket_path,
length: getLength(item),
thumbnail: "/resources/placeholders/audio_placeholder.png",
isMine: true,
isNew:
previousItems.length > 0
? checkIsNew(previousItems, item.token)
: false,
// isBookmarked?: boolean;
};
return morphedItem;
});
userAudioItems.value = morphedNewSet;
};
export const cancelNewFromAudioItem = (mediaId: string) => {
if (!userAudioItems.value || userAudioItems.value.length === 0) {
return;
}
const newList = userAudioItems.value.map((item) => {
if (item.media_id === mediaId) item.isNew = false;
return item;
});
userAudioItems.value = [...newList];
};
export const updateAudioItemLength = (mediaId: string, duration: number) => {
if (!userAudioItems.value || userAudioItems.value.length === 0) {
return;
}
const newList = userAudioItems.value.map((item) => {
if (item.media_id === mediaId) item.length = duration;
return item;
});
userAudioItems.value = [...newList];
};
// Helper functions to massage Media Info to AudioMediaItem
// that's usable as a signal.
function getTitle(item: MediaInfo) {
// console.log(item);
if (item.maybe_title) return item.maybe_title;
if (item.origin && item.origin.maybe_model && item.origin.maybe_model.title)
return item.origin.maybe_model.title;
return "Media Audio";
}
function getCategory(item: MediaInfo) {
if (
item.origin &&
item.origin.product_category &&
item.origin.product_category !== "unknown"
)
return item.origin.product_category;
if (item.origin_category) return item.origin_category;
return "unknown";
}
function checkIsNew(previousItems: AudioMediaItem[], token: string) {
const foundItem = previousItems.find((item) => {
return token === item.media_id && item.isNew !== true;
});
return foundItem === undefined;
}
function getLength(item: MediaInfo) {
return item.maybe_duration_millis
? (item.maybe_duration_millis / 1000) * 60
: undefined;
}
+1
View File
@@ -1,5 +1,6 @@
export * from "./authentication";
export * from "./activeJobs";
export * from "./backgroundLoadedMedia";
export * from "./global";
export * from "./loadingBar";
export * from "./scene"; // THIS IS SHOULE BE PAGE LEVEL