Merge pull request #244 from storytold/bflat-fy-vc-pitch-control

Pitch shift controls
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-05-19 02:51:00 -04:00
commit 3cec9562eb
7 files changed
+285 -1

No files matched your search

@@ -1,9 +1,19 @@
import { ApiConfig } from "@storyteller/components";
export enum EnqueueVoiceConversionFrequencyMethod {
Crepe = "crepe",
Dio = "dio",
Harvest = "harvest",
}
export interface EnqueueVoiceConversionRequest {
uuid_idempotency_token: string,
voice_conversion_model_token: string,
source_media_upload_token: string,
// Optional args
override_f0_method?: EnqueueVoiceConversionFrequencyMethod,
transpose?: number,
}
export interface EnqueueVoiceConversionSuccessResponse {
+1
View File
@@ -45,6 +45,7 @@
"react-dom": "^16.13.1",
"react-drag-drop-files": "^2.3.9",
"react-i18next": "^11.15.4",
"react-range": "^1.8.14",
"react-reveal": "^1.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "5.0.0",
@@ -724,6 +724,10 @@ styles with important tags:
box-shadow: none;
cursor: pointer;
&.no-padding {
padding-left: 0.5rem;
}
&:hover {
background-color: lighten($input-dark, 5%);
border-color: lighten($input-dark, 5%) !important;
@@ -866,6 +870,13 @@ styles with important tags:
fill: lighten($secondary, 5%);
}
.tippy-box[data-theme~="range-slider"] {
margin-top: -5px;
background-color: lighten($panel, 10%);
color: $white;
font-size: 13px !important;
}
.sliding-content {
overflow: hidden;
opacity: 0;
@@ -1269,6 +1280,19 @@ styles with important tags:
.alert-info {
font-size: 14px;
}
.range-slider-value {
text-align: center;
max-width: 50px;
padding: 6px;
border-radius: 8px;
pointer-events: none;
background-color: lighten($panel, 5%) !important;
border-width: 1px;
border-color: rgba(255, 255, 255, 0.12) !important;
color: rgba($white, 0.85) !important;
font-weight: 500 !important;
}
}
select {
@@ -1404,6 +1428,10 @@ textarea {
height: 450px;
}
.session-vc-section {
height: 565px;
}
.session-tts-section::-webkit-scrollbar {
width: 10px;
}
@@ -23,6 +23,7 @@ import {
import { VcModelListSearch } from "./components/VcModelListSearchComponent";
import {
EnqueueVoiceConversion,
EnqueueVoiceConversionFrequencyMethod,
EnqueueVoiceConversionIsSuccess,
EnqueueVoiceConversionRequest,
} from "@storyteller/components/src/api/voice_conversion/EnqueueVoiceConversion";
@@ -33,6 +34,8 @@ import {
} from "@storyteller/components/src/jobs/InferenceJob";
import { SessionVoiceConversionResultsList } from "../../../_common/SessionVoiceConversionResultsList";
import { Link } from "react-router-dom";
import PitchShiftComponent from "./components/PitchShiftComponent";
import PitchEstimateMethodComponent from "./components/PitchEstimateMethodComponent";
interface Props {
sessionWrapper: SessionWrapper;
@@ -58,6 +61,7 @@ interface Props {
function VcModelListPage(props: Props) {
usePrefixedDocumentTitle("Voice Conversion");
const [convertLoading, setConvertLoading] = useState(false);
const [canConvert, setCanConvert] = useState(false);
@@ -69,6 +73,11 @@ function VcModelListPage(props: Props) {
uuidv4()
);
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.
@@ -147,6 +156,14 @@ function VcModelListPage(props: Props) {
source_media_upload_token: mediaUploadToken,
};
if (semitones !== 0) {
request.transpose = semitones;
}
if (maybeF0MethodOverride !== undefined) {
request.override_f0_method = maybeF0MethodOverride;
}
Analytics.voiceConversionGenerate(
props.maybeSelectedVoiceConversionModel.token
);
@@ -167,6 +184,16 @@ function VcModelListPage(props: Props) {
ev.preventDefault();
};
const handlePitchChange = (value: any) => {
setSemitones(value);
changeConvertIdempotencyToken();
};
const handlePitchMethodChange = (value: any) => {
setMaybeF0MethodOverride(value);
changeConvertIdempotencyToken();
};
const speakButtonClass = convertLoading
? "btn btn-primary w-100 disabled"
: "btn btn-primary w-100";
@@ -280,6 +307,26 @@ function VcModelListPage(props: Props) {
</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>
</div>
{/*<div>
<label className="sub-title">
Or pick from your audio collection (5 files)
@@ -349,6 +396,27 @@ function VcModelListPage(props: Props) {
</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>
</div>
<div>
<label className="sub-title">Convert Audio</label>
@@ -381,7 +449,7 @@ function VcModelListPage(props: Props) {
/>
Session V2V Results
</h4>
<div className="d-flex flex-column gap-3 session-tts-section">
<div className="d-flex flex-column gap-3 session-tts-section session-vc-section">
<SessionVoiceConversionResultsList
inferenceJobs={
props.inferenceJobsByCategory.get(
@@ -0,0 +1,77 @@
import React from "react";
import Select from "react-select";
import { EnqueueVoiceConversionFrequencyMethod } from "@storyteller/components/src/api/voice_conversion/EnqueueVoiceConversion";
interface PitchShiftProps {
pitchMethod?: EnqueueVoiceConversionFrequencyMethod,
onMethodChange: (newMethod?: EnqueueVoiceConversionFrequencyMethod) => void;
}
const dropdownFieldClass = {
control: (state: any) =>
state.isFocused
? "select-search no-padding focused rounded"
: "select-search no-padding rounded",
option: (state: any) => (state.isFocused ? "select-option" : "select-option"),
input: (state: any) => (state.isFocused ? "select-input" : "select-input"),
placeholder: (state: any) =>
state.isFocused ? "select-placeholder" : "select-placeholder",
singleValue: (state: any) =>
state.isFocused ? "select-value" : "select-value",
menu: (state: any) =>
state.isFocused ? "select-container" : "select-container",
indicatorSeparator: (state: any) =>
state.isFocused ? "select-separator" : "select-separator",
};
function PitchEstimateMethodComponent(props: PitchShiftProps) {
const handleMethodChange = (option: any, actionMeta: any) => {
const pitchMethodName = option?.value;
let pitchMethod = undefined;
switch (pitchMethodName) {
case "crepe":
pitchMethod = EnqueueVoiceConversionFrequencyMethod.Crepe;
break;
case "harvest":
pitchMethod = EnqueueVoiceConversionFrequencyMethod.Harvest;
break;
case "dio":
pitchMethod = EnqueueVoiceConversionFrequencyMethod.Dio;
break;
}
props.onMethodChange(pitchMethod);
};
interface DropdownOption {
label: string;
value: EnqueueVoiceConversionFrequencyMethod;
}
const options : DropdownOption[] = [
{ label: "CREPE", value: EnqueueVoiceConversionFrequencyMethod.Crepe },
{ label: "Harvest", value: EnqueueVoiceConversionFrequencyMethod.Harvest },
{ label: "DIO", value: EnqueueVoiceConversionFrequencyMethod.Dio },
];
const selectedOption = options.find((option) => option.value === props.pitchMethod) || options[0];
return (
<>
<div>
<Select
value={selectedOption} // Controlled components use "value" instead of "defaultValue".
options={options}
classNames={dropdownFieldClass}
//placeholder={options[0]}
isSearchable={false}
onChange={handleMethodChange}
/>
</div>
</>
);
}
export default PitchEstimateMethodComponent;
@@ -0,0 +1,89 @@
import React from "react";
import { Range } from "react-range";
import Tippy from "@tippyjs/react";
interface PitchShiftProps {
min: number;
max: number;
step: number;
value: number;
onPitchChange: (value: number) => void;
}
function PitchShiftComponent({
min,
max,
step,
value,
onPitchChange,
}: PitchShiftProps) {
const handlePitchChange = (newValue: number[]) => {
onPitchChange(newValue[0]);
};
const renderTrack = ({ props, children }: any) => (
<div
{...props}
style={{
...props.style,
height: "8px",
width: "100%",
backgroundColor: "#4f4f66",
borderRadius: "4px",
}}
>
{children}
</div>
);
const renderThumb = ({ props, isDragged, isHovered }: any) => (
<Tippy
content="Semitones"
placement="bottom"
theme="range-slider"
arrow={false}
>
<div
{...props}
style={{
...props.style,
height: "22px",
width: "22px",
borderRadius: "50%",
backgroundColor: isHovered ? "#fff" : "#e66462",
display: "flex",
justifyContent: "center",
alignItems: "center",
outline: "none",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.15)", // Add soft shadow on hover
}}
></div>
</Tippy>
);
return (
<>
<div className="d-flex gap-3 align-items-center">
<div className="flex-grow-1">
<Range
step={step}
min={min}
max={max}
values={[value]} // Changed from `values` to `[value]`
onChange={handlePitchChange}
renderTrack={renderTrack}
renderThumb={renderThumb}
/>
</div>
<input
className="form-control range-slider-value"
value={value.toFixed(0)}
disabled
></input>
</div>
</>
);
}
export default PitchShiftComponent;
+11
View File
@@ -4498,6 +4498,7 @@ __metadata:
dependencies:
craco: ^0.0.3
lerna: ^4.0.0
react-range: ^1.8.14
yarn: ^1.22.19
languageName: unknown
linkType: soft
@@ -17132,6 +17133,16 @@ __metadata:
languageName: node
linkType: hard
"react-range@npm:^1.8.14":
version: 1.8.14
resolution: "react-range@npm:1.8.14"
peerDependencies:
react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0
react-dom: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0
checksum: 7020c011dc293db46ede9b7cb014dbfe71d59f96aad9cf9b12e9de26c2b600c504e11d39368af8c539c4a3dc1f93e966745063e0b494c388c28fbadd338164bb
languageName: node
linkType: hard
"react-reconciler@npm:0.25.1":
version: 0.25.1
resolution: "react-reconciler@npm:0.25.1"