mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
generate audio panel
This commit is contained in:
1 parent
76a8e10fab
commit
fa9e94b0a3
4 files changed
+428
-34
No files matched your search
@@ -1491,7 +1491,7 @@ textarea {
|
||||
}
|
||||
|
||||
.session-vc-section {
|
||||
height: 565px;
|
||||
height: 660px;
|
||||
}
|
||||
|
||||
.session-tts-section::-webkit-scrollbar {
|
||||
|
||||
@@ -524,6 +524,9 @@ class PageContainer extends React.Component<Props, State> {
|
||||
this.props.sessionSubscriptionsWrapper
|
||||
}
|
||||
inferenceJobsByCategory={this.props.inferenceJobsByCategory}
|
||||
setMaybeSelectedInferenceJob={
|
||||
this.props.maybeSelectedVoiceConversionModel
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import Panel from "components/common/Panel/Panel";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBarsStaggered,
|
||||
faRightLeft,
|
||||
faWaveformLines,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { SessionVoiceConversionResultsList } from "v2/view/_common/SessionVoiceConversionResultsList";
|
||||
import {
|
||||
FrontendInferenceJobType,
|
||||
InferenceJob,
|
||||
} from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import UploadComponent from "./vc_model_list/components/UploadComponent";
|
||||
import {
|
||||
EnqueueVoiceConversion,
|
||||
EnqueueVoiceConversionFrequencyMethod,
|
||||
EnqueueVoiceConversionIsSuccess,
|
||||
EnqueueVoiceConversionRequest,
|
||||
} from "@storyteller/components/src/api/voice_conversion/EnqueueVoiceConversion";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import {
|
||||
VoiceConversionModelListItem,
|
||||
ListVoiceConversionModels,
|
||||
} from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
|
||||
import { Analytics } from "common/Analytics";
|
||||
import LoadingIcon from "components/common/FileActions/LoadingIcon";
|
||||
import PitchEstimateMethodComponent from "./vc_model_list/components/PitchEstimateMethodComponent";
|
||||
import PitchShiftComponent from "./vc_model_list/components/PitchShiftComponent";
|
||||
import RecordComponent from "./vc_model_list/components/RecordComponent";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
interface VcGenerateAudioPanelProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
|
||||
voiceConversionModels: Array<VoiceConversionModelListItem>;
|
||||
setVoiceConversionModels: (
|
||||
ttsVoices: Array<VoiceConversionModelListItem>
|
||||
) => void;
|
||||
|
||||
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
|
||||
setMaybeSelectedVoiceConversionModel: (
|
||||
maybeSelectedVoiceConversionModel: VoiceConversionModelListItem
|
||||
) => void;
|
||||
|
||||
enqueueInferenceJob: (
|
||||
jobToken: string,
|
||||
frontendInferenceJobType: FrontendInferenceJobType
|
||||
) => void;
|
||||
inferenceJobs: Array<InferenceJob>;
|
||||
inferenceJobsByCategory: Map<FrontendInferenceJobType, Array<InferenceJob>>;
|
||||
}
|
||||
|
||||
export default function VcGenerateAudioPanel(props: VcGenerateAudioPanelProps) {
|
||||
const [convertLoading, setConvertLoading] = useState(false);
|
||||
const [canConvert, setCanConvert] = useState(false);
|
||||
|
||||
const [mediaUploadToken, setMediaUploadToken] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [convertIdempotencyToken, setConvertIdempotencyToken] = useState(
|
||||
uuidv4()
|
||||
);
|
||||
|
||||
const [autoConvertF0, setAutoConvertF0] = useState(false);
|
||||
|
||||
const [maybeF0MethodOverride, setMaybeF0MethodOverride] = useState<
|
||||
EnqueueVoiceConversionFrequencyMethod | undefined
|
||||
>(undefined);
|
||||
|
||||
const [semitones, setSemitones] = useState(0);
|
||||
|
||||
// NB: Something of a UI hack here.
|
||||
// The 3rd party microphone component doesn't let you clear it, so we emulate form clearing
|
||||
// with this variable.
|
||||
const [formIsCleared, setFormIsCleared] = useState(false);
|
||||
|
||||
let {
|
||||
setVoiceConversionModels,
|
||||
voiceConversionModels,
|
||||
maybeSelectedVoiceConversionModel,
|
||||
setMaybeSelectedVoiceConversionModel,
|
||||
} = props;
|
||||
|
||||
const vcModelsLoaded = voiceConversionModels.length > 0;
|
||||
|
||||
const listModels = useCallback(async () => {
|
||||
if (vcModelsLoaded) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const models = await ListVoiceConversionModels();
|
||||
if (models) {
|
||||
setVoiceConversionModels(models);
|
||||
if (!maybeSelectedVoiceConversionModel && models.length > 0) {
|
||||
let model = models[0];
|
||||
const featuredModels = models.filter((m) => m.is_front_page_featured);
|
||||
if (featuredModels.length > 0) {
|
||||
// Random featured model
|
||||
model =
|
||||
featuredModels[Math.floor(Math.random() * featuredModels.length)];
|
||||
}
|
||||
setMaybeSelectedVoiceConversionModel(model);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
setVoiceConversionModels,
|
||||
maybeSelectedVoiceConversionModel,
|
||||
setMaybeSelectedVoiceConversionModel,
|
||||
vcModelsLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
listModels();
|
||||
}, [listModels]);
|
||||
|
||||
const changeConvertIdempotencyToken = () => {
|
||||
setConvertIdempotencyToken(uuidv4());
|
||||
};
|
||||
|
||||
const handleVoiceConversion = async () => {
|
||||
if (
|
||||
props.maybeSelectedVoiceConversionModel === undefined ||
|
||||
mediaUploadToken === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setConvertLoading(true);
|
||||
|
||||
let request: EnqueueVoiceConversionRequest = {
|
||||
uuid_idempotency_token: convertIdempotencyToken,
|
||||
voice_conversion_model_token:
|
||||
props.maybeSelectedVoiceConversionModel.token,
|
||||
source_media_upload_token: mediaUploadToken,
|
||||
};
|
||||
|
||||
if (semitones !== 0) {
|
||||
request.transpose = semitones;
|
||||
}
|
||||
|
||||
if (maybeF0MethodOverride !== undefined) {
|
||||
request.override_f0_method = maybeF0MethodOverride;
|
||||
}
|
||||
|
||||
if (autoConvertF0) {
|
||||
request.auto_predict_f0 = true;
|
||||
}
|
||||
|
||||
Analytics.voiceConversionGenerate(
|
||||
props.maybeSelectedVoiceConversionModel.token
|
||||
);
|
||||
|
||||
let result = await EnqueueVoiceConversion(request);
|
||||
|
||||
if (EnqueueVoiceConversionIsSuccess(result)) {
|
||||
props.enqueueInferenceJob(
|
||||
result.inference_job_token,
|
||||
FrontendInferenceJobType.VoiceConversion
|
||||
);
|
||||
}
|
||||
|
||||
setConvertLoading(false);
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
const handlePitchChange = (value: any) => {
|
||||
setSemitones(value);
|
||||
changeConvertIdempotencyToken();
|
||||
};
|
||||
|
||||
const handlePitchMethodChange = (value: any) => {
|
||||
setMaybeF0MethodOverride(value);
|
||||
changeConvertIdempotencyToken();
|
||||
};
|
||||
|
||||
const handleAutoF0Change = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (ev.target as HTMLInputElement).checked;
|
||||
setAutoConvertF0(value);
|
||||
changeConvertIdempotencyToken();
|
||||
};
|
||||
|
||||
const speakButtonClass = convertLoading
|
||||
? "btn btn-primary w-100 disabled"
|
||||
: "btn btn-primary w-100";
|
||||
|
||||
const enableConvertButton =
|
||||
canConvert &&
|
||||
mediaUploadToken !== undefined &&
|
||||
props.maybeSelectedVoiceConversionModel !== undefined;
|
||||
|
||||
return (
|
||||
<Panel padding mb>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="row g-5">
|
||||
<div className="col-12 col-lg-6 d-flex flex-column gap-4">
|
||||
<h4>
|
||||
<FontAwesomeIcon icon={faWaveformLines} className="me-3" />
|
||||
Use Voice
|
||||
</h4>
|
||||
<ul className="nav nav-tabs nav-vc" id="myTab" role="tablist">
|
||||
<li className="nav-item w-100" role="presentation">
|
||||
<button
|
||||
className="nav-link active w-100 pt-0"
|
||||
id="prerecorded-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#prerecorded"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="prerecorded"
|
||||
aria-selected="true"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item w-100" role="presentation">
|
||||
<button
|
||||
className="nav-link w-100 pt-0"
|
||||
id="recordaudio-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#recordaudio"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="recordaudio"
|
||||
aria-selected="false"
|
||||
>
|
||||
Microphone
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="tab-content" id="myTabContent">
|
||||
<div
|
||||
className="tab-pane fade show active"
|
||||
id="prerecorded"
|
||||
role="tabpanel"
|
||||
aria-labelledby="prerecorded-tab"
|
||||
>
|
||||
<div className="d-flex flex-column gap-4 h-100">
|
||||
<div>
|
||||
<label className="sub-title">Upload Input Audio</label>
|
||||
<div className="d-flex flex-column gap-3 upload-component">
|
||||
<UploadComponent
|
||||
setMediaUploadToken={setMediaUploadToken}
|
||||
formIsCleared={formIsCleared}
|
||||
setFormIsCleared={setFormIsCleared}
|
||||
setCanConvert={setCanConvert}
|
||||
changeConvertIdempotencyToken={
|
||||
changeConvertIdempotencyToken
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Pitch Control</label>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<PitchEstimateMethodComponent
|
||||
pitchMethod={maybeF0MethodOverride}
|
||||
onMethodChange={handlePitchMethodChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PitchShiftComponent
|
||||
min={-36}
|
||||
max={36}
|
||||
step={1}
|
||||
value={semitones}
|
||||
onPitchChange={handlePitchChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
id="autoF0Checkbox"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={autoConvertF0}
|
||||
onChange={handleAutoF0Change}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="autoF0Checkbox"
|
||||
>
|
||||
Auto F0 (off for singing, on for speech)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Convert Audio</label>
|
||||
|
||||
<div className="d-flex gap-3">
|
||||
<button
|
||||
className={speakButtonClass}
|
||||
onClick={handleVoiceConversion}
|
||||
type="submit"
|
||||
disabled={!enableConvertButton}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRightLeft} className="me-2" />
|
||||
Convert
|
||||
{convertLoading && <LoadingIcon />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="tab-pane fade"
|
||||
id="recordaudio"
|
||||
role="tabpanel"
|
||||
aria-labelledby="recordaudio-tab"
|
||||
>
|
||||
<div className="d-flex flex-column gap-4 h-100">
|
||||
<div>
|
||||
<label className="sub-title">Record Audio</label>
|
||||
<div className="d-flex flex-column gap-3 upload-component">
|
||||
<RecordComponent
|
||||
setMediaUploadToken={setMediaUploadToken}
|
||||
formIsCleared={formIsCleared}
|
||||
setFormIsCleared={setFormIsCleared}
|
||||
setCanConvert={setCanConvert}
|
||||
changeConvertIdempotencyToken={
|
||||
changeConvertIdempotencyToken
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Pitch Control</label>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<PitchEstimateMethodComponent
|
||||
pitchMethod={maybeF0MethodOverride}
|
||||
onMethodChange={handlePitchMethodChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PitchShiftComponent
|
||||
min={-36}
|
||||
max={36}
|
||||
step={1}
|
||||
value={semitones}
|
||||
onPitchChange={handlePitchChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
id="autoF0CheckboxMic"
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={autoConvertF0}
|
||||
onChange={handleAutoF0Change}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="autoF0CheckboxMic"
|
||||
>
|
||||
Auto F0 (off for singing, on for speech)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Convert Audio</label>
|
||||
|
||||
<div className="d-flex gap-3">
|
||||
<button
|
||||
className={speakButtonClass}
|
||||
onClick={handleVoiceConversion}
|
||||
type="submit"
|
||||
disabled={!enableConvertButton}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRightLeft} className="me-2" />
|
||||
Convert
|
||||
{convertLoading && <LoadingIcon />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<h4 className="mb-4">
|
||||
<FontAwesomeIcon icon={faBarsStaggered} className="me-3" />
|
||||
Session V2V Results
|
||||
</h4>
|
||||
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
|
||||
<SessionVoiceConversionResultsList
|
||||
inferenceJobs={
|
||||
props.inferenceJobsByCategory.get(
|
||||
FrontendInferenceJobType.VoiceConversion
|
||||
)!
|
||||
}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
faBarsStaggered,
|
||||
faEdit,
|
||||
faEye,
|
||||
faMemo,
|
||||
faMemoCircleInfo,
|
||||
faMessages,
|
||||
faTrash,
|
||||
faVolumeHigh,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import Panel from "components/common/Panel/Panel";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { SessionVoiceConversionResultsList } from "v2/view/_common/SessionVoiceConversionResultsList";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
|
||||
import { CommentComponent } from "v2/view/_common/comments/CommentComponent";
|
||||
@@ -22,8 +18,10 @@ import { RatingButtons } from "v2/view/_common/ratings/RatingButtons";
|
||||
import { RatingStats } from "v2/view/_common/ratings/RatingStats";
|
||||
import ShareButton from "components/common/ShareButton/ShareButton";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import VcGenerateAudioPanel from "../VcGenerateAudioPanel";
|
||||
|
||||
interface VcModelViewPageProps {
|
||||
setMaybeSelectedInferenceJob: any;
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
@@ -148,34 +146,16 @@ export default function VcModelViewPage(props: VcModelViewPageProps) {
|
||||
extras={shareButton}
|
||||
/>
|
||||
|
||||
<Panel padding mb>
|
||||
<div className="row g-5">
|
||||
<div className="col-12 col-lg-6">
|
||||
<h4 className="mb-3">
|
||||
<FontAwesomeIcon icon={faVolumeHigh} className="me-3" />
|
||||
Use Voice
|
||||
</h4>
|
||||
{/* have to replace this below with Voice conversion stuff instead of the textarea */}
|
||||
<div className="opacity-50">voice to voice components here</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<h4 className="mb-3">
|
||||
<FontAwesomeIcon icon={faBarsStaggered} className="me-3" />
|
||||
Session V2V Results
|
||||
</h4>
|
||||
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
|
||||
<SessionVoiceConversionResultsList
|
||||
inferenceJobs={
|
||||
props.inferenceJobsByCategory.get(
|
||||
FrontendInferenceJobType.VoiceConversion
|
||||
)!
|
||||
}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<VcGenerateAudioPanel
|
||||
inferenceJobsByCategory={props.inferenceJobsByCategory}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
setVoiceConversionModels={() => {}}
|
||||
voiceConversionModels={[]}
|
||||
setMaybeSelectedVoiceConversionModel={() => {}}
|
||||
enqueueInferenceJob={() => {}}
|
||||
inferenceJobs={[]}
|
||||
/>
|
||||
|
||||
{modelDescription && (
|
||||
<Panel padding mb>
|
||||
|
||||
Reference in new issue
Block a user