diff --git a/src/plugins/customVoiceFilter/ConfirmModal.tsx b/src/plugins/customVoiceFilter/ConfirmModal.tsx index d95ef411e..2a9580f93 100644 --- a/src/plugins/customVoiceFilter/ConfirmModal.tsx +++ b/src/plugins/customVoiceFilter/ConfirmModal.tsx @@ -37,7 +37,7 @@ function ConfirmModal({ modalProps, message, accept, close }: ConfirmModalProps) - + {message} diff --git a/src/plugins/customVoiceFilter/CreateVoiceFilterModal.tsx b/src/plugins/customVoiceFilter/CreateVoiceFilterModal.tsx index 9a98f083d..a6488a0c6 100644 --- a/src/plugins/customVoiceFilter/CreateVoiceFilterModal.tsx +++ b/src/plugins/customVoiceFilter/CreateVoiceFilterModal.tsx @@ -57,7 +57,7 @@ function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoice }, [voiceFilter]); const keyOptions: SelectOption[] = useMemo(() => - [{ value: "", label: "(empty)" }, ...Object.keys(voices).map(name => ({ value: name, label: name }))], + [{ value: "", label: "(empty)" }, ...(voices ? Object.keys(voices).map(name => ({ value: name, label: name })) : [])], []); return ( @@ -68,7 +68,7 @@ function CreateVoiceFilterModal({ modalProps, close, defaultValue }: CreateVoice - + Name* diff --git a/src/plugins/customVoiceFilter/ErrorModal.tsx b/src/plugins/customVoiceFilter/ErrorModal.tsx index 836b4c33d..aeb22d80f 100644 --- a/src/plugins/customVoiceFilter/ErrorModal.tsx +++ b/src/plugins/customVoiceFilter/ErrorModal.tsx @@ -36,7 +36,7 @@ function ErrorModal({ modalProps, close, message }: ErrorModalProps): JSX.Elemen - + {message} diff --git a/src/plugins/customVoiceFilter/HelpModal.tsx b/src/plugins/customVoiceFilter/HelpModal.tsx index 2eb16388a..7c90c3edd 100644 --- a/src/plugins/customVoiceFilter/HelpModal.tsx +++ b/src/plugins/customVoiceFilter/HelpModal.tsx @@ -4,14 +4,13 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { CodeBlock } from "@components/CodeBlock"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Button, Flex, Forms, Text } from "@webpack/common"; +import { Button, Flex, Forms } from "@webpack/common"; import { JSX } from "react"; -import { templateVoicepack } from "."; +import { templateVoicepack, voices } from "."; +import { Markdown } from "./Markdown"; import { downloadFile } from "./utils"; -import { openVoiceFiltersModal } from "./VoiceFiltersModal"; export function openHelpModal(): string { const key = openModal(modalProps => ( @@ -29,6 +28,16 @@ interface HelpModalProps { } function HelpModal({ modalProps, close }: HelpModalProps): JSX.Element { + const description = `To build your own voicepack, you need to have a voicepack file. You can download one from the template or look at this tutorial. + +The voicepack file is a json file that contains the voicepack data. +A voicepack may have one or multiple voices. Each voice is an object with the following properties: +\`\`\`json +${templateVoicepack} +\`\`\`*Style Key must be "" or one of the following: ${voices ? [...new Set(Object.values(voices).map(({ styleKey }) => styleKey))].join(", ") : ""}* + +Once you have the voicepack file, you can use the to manage your voicepacks.`; + return ( @@ -37,20 +46,16 @@ function HelpModal({ modalProps, close }: HelpModalProps): JSX.Element { - - - To build your own voicepack, you need to have a voicepack file. You can download one from the template or look at this tutorial. - The voicepack file is a json file that contains the voicepack data. You can find the template { - downloadFile("voicepack-template.json", templateVoicepack); - }}>here - Once you have the voicepack file, you can use the Voice Filters Management Menu to manage your voicepacks. - A voicepack may have one or multiple voices. Each voice is an object with the following properties: - - Style Key must be "" or one of the following: skye, quinn, axel, sebastien, megaphone, robot, tunes, ghost, spacebunny, justus, harper, villain, solara, cave, deepfried - + + - + + + + ); diff --git a/src/plugins/customVoiceFilter/Icons.tsx b/src/plugins/customVoiceFilter/Icons.tsx index 939c1099e..60f5a2ca7 100644 --- a/src/plugins/customVoiceFilter/Icons.tsx +++ b/src/plugins/customVoiceFilter/Icons.tsx @@ -5,55 +5,89 @@ */ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { JSX } from "react"; +import { JSX, SVGProps } from "react"; -import { VoiceFilterStyles } from "./index"; import { openVoiceFiltersModal } from "./VoiceFiltersModal"; -export function DownloadIcon(): JSX.Element { + + +interface IconProps extends SVGProps { } + +export function DownloadIcon(props: IconProps): JSX.Element { return ( - {name} + ), + requiredFirstCharacters: ["<"] + }, + image: { + ...Parser.defaultRules.link, + match: source => imageRegex.exec(source), + parse: ([, title, target]) => ({ title, target }), + react: ({ title, target }) =>
+ {title} +
, + requiredFirstCharacters: ["!"] + } + }; + + const builtinRules = new Set([...Object.keys(DiscordRules), ...Object.keys(AdvancedRules)]); + + for (const rule of builtinRules) { + customRules[rule] = { + ...DiscordRules[rule], + ...AdvancedRules[rule], + }; + } + + return (Parser as any).reactParserFor(customRules); +}); + +interface MarkdownProps extends Omit { + content: string; + markdownRules?: Partial; +} + + +export function Markdown({ content, markdownRules = defaultRules, className, ...props }: MarkdownProps) { + return
+ {parser(content, false, markdownRules)} +
; +} diff --git a/src/plugins/customVoiceFilter/VoiceFiltersModal.tsx b/src/plugins/customVoiceFilter/VoiceFiltersModal.tsx index 42e22957e..e77f5cb04 100644 --- a/src/plugins/customVoiceFilter/VoiceFiltersModal.tsx +++ b/src/plugins/customVoiceFilter/VoiceFiltersModal.tsx @@ -4,16 +4,17 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { PencilIcon } from "@components/Icons"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { PluginNative } from "@utils/types"; -import { Button, Flex, Forms, Text, TextInput, useEffect, useState } from "@webpack/common"; +import { Button, Flex, Forms, Text, TextInput, Tooltip, useEffect, useState } from "@webpack/common"; import { JSX } from "react"; import { openCreateVoiceModal } from "./CreateVoiceFilterModal"; import { openHelpModal } from "./HelpModal"; -import { DownloadIcon, DownloadingIcon, PauseIcon, PlayIcon } from "./Icons"; +import { DownloadIcon, DownloadingIcon, PauseIcon, PlayIcon, RefreshIcon, TrashIcon } from "./Icons"; import { downloadCustomVoiceModel, getClient, IVoiceFilter, useVoiceFiltersStore, VoiceFilterStyles } from "./index"; -import { useAudio } from "./utils"; +import { cl, useAudio } from "./utils"; import { openWikiHomeModal } from "./WikiHomeModal"; const Native = VencordNative.pluginHelpers.CustomVoiceFilters as PluginNative; @@ -54,7 +55,7 @@ function VoiceFiltersModal({ modalProps, close, accept }: VoiceFiltersModalProps - + Download a voicepack from a url or paste a voicepack data here: { +
{ if (!voiceFilter.available) return; // download and preview if downloaded @@ -126,16 +137,22 @@ function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element { if (res.success) setModelState({ status: "downloaded", downloadedBytes: 0 }); } }}> -
+
-
+
- {client === "desktop" && voiceFilter.available && modelState.status === "not_downloaded" &&
} - {client === "desktop" && voiceFilter.available && modelState.status === "downloading" &&
} - {((client === "desktop" && voiceFilter.available && modelState.status === "downloaded") || (client === "web" && voiceFilter.available)) &&
- isPlaying ? stopSound() : playSound() - }>{isPlaying ? : }
} + {voiceFilter.available && <> + {client === "desktop" && modelState.status === "not_downloaded" &&
} + {client === "desktop" && modelState.status === "downloading" &&
} + {((client === "desktop" && modelState.status === "downloaded") || client === "web") &&
+ isPlaying ? stopSound() : playSound() + }>{isPlaying ? : }
} + }
@@ -143,30 +160,38 @@ function VoiceFilter(voiceFilter: IVoiceFilter): JSX.Element {
- {voiceFilter.available && ((client === "desktop" && modelState.status === "downloaded") || (client === "web")) ? ( + {voiceFilter.available && ((client === "desktop" && modelState.status === "downloaded") || (client === "web")) && ( <> -
updateById(id)} className={className} role="button" tabIndex={-1}> - -
-
deleteById(id)} className={className} role="button" tabIndex={-1} style={{ left: "65px" }}> - -
-
exportIndividualVoice(id)} className={className} role="button" tabIndex={-1} style={{ top: "65px" }}> - -
-
openCreateVoiceModal(voiceFilter)} className={className} role="button" tabIndex={-1} style={{ top: "65px", left: "65px" }}> - -
- ) : <>} + + {({ ...props }) => +
updateById(id)}> + +
+ } +
+ + {({ ...props }) => +
deleteById(id)}> + +
+ } +
+ + {({ ...props }) => +
exportIndividualVoice(id)}> + +
+ } +
+ + {({ ...props }) => +
openCreateVoiceModal(voiceFilter)} > + +
+ } +
+ + )}
); } diff --git a/src/plugins/customVoiceFilter/WikiHomeModal.tsx b/src/plugins/customVoiceFilter/WikiHomeModal.tsx index 7a52c4a8f..9acb45eaa 100644 --- a/src/plugins/customVoiceFilter/WikiHomeModal.tsx +++ b/src/plugins/customVoiceFilter/WikiHomeModal.tsx @@ -5,11 +5,49 @@ */ import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Button, Card, Forms, Text, useState } from "@webpack/common"; +import { Button, Card, Flex, Forms, Text, useState } from "@webpack/common"; -import { openCreateVoiceModal } from "./CreateVoiceFilterModal"; -import { openHelpModal } from "./HelpModal"; -import { openVoiceFiltersModal } from "./VoiceFiltersModal"; +import { ChevronIcon } from "./Icons"; +import { Markdown } from "./Markdown"; +import { cl } from "./utils"; + +interface Section { + title: string; + content: string; +} + +const sections: Section[] = [ + { + title: "How to install a voicepack", + content: "To install a voicepack, you need to paste the voicepack url in the " + }, + { + title: "How to create a voicepack", + content: `You have two methods to create a voicepack: +1. Use the (recommended) +2. Use the (advanced)` + }, + { + title: "How does it work?", + content: `Discord actually uses a Python project named [Retrieval-based Voice Conversion](https://github.com/RVC-Project/Retrieval-based-Voice-Conversion) to convert your voice into the voice model you picked. +This voice cloning technology allows an audio input to be converted into a different voice, with a high degree of accuracy. +Actually, Discord uses ONNX files to run the model, for a better performance and less CPU usage. +![img](https://fox3000foxy.com/voicepacks/assets/working.png)` + }, + { + title: "How to create an ONNX from an existing RVC model?", + content: `RVC models can be converted to ONNX files using the [W-Okada Software](https://github.com/w-okada/voice-changer/). +MMVCServerSio is software that is issued from W-Okada Software, and can be downloaded [here](https://huggingface.co/datasets/Derur/all-portable-ai-in-one-url/blob/main/HZ/MMVCServerSIO.7z). +Thats the actual software that does exports RVC models to ONNX files. +Just load your model inside MMVCServerSio, and click on "Export ONNX": +![img](https://fox3000foxy.com/voicepacks/assets/export-1.png)![img](https://fox3000foxy.com/voicepacks/assets/export-2.png) +Enjoy you now have a ONNX model file for your voicepack!` + }, + { + title: "How to train my own voice model?", + content: "Refers to [this video](https://www.youtube.com/watch?v=tnfqIQ11Qek&ab_channel=AISearch) and convert it to ONNX." + } +]; interface WikiHomeModalProps { modalProps: ModalProps; @@ -19,49 +57,21 @@ interface WikiHomeModalProps { export function WikiHomeModal({ modalProps, close, accept }: WikiHomeModalProps) { return ( - + Wiki Home - -

- Here are some tutorials and guides about the Custom Voice Filter Plugin: -

- To install a voicepack, you need to paste the voicepack url in the openVoiceFiltersModal()}>main menu. - } />
- - You have two methods to create a voicepack:
- 1. Use the openCreateVoiceModal()}>voicepack creator modal (recommended)
- 2. Use the openHelpModal()}>Help Modal (advanced) - - } />
- - Discord actually uses a Python project named Retrieval-based Voice Conversion
to convert your voice into the voice model you picked.

- This voice cloning technology allows an audio input to be converted into a different voice, with a high degree of accuracy.
- Actually, Discord uses ONNX files to run the model, for a better performance and less CPU usage. - - } />
- - RVC models can be converted to ONNX files using the W-Okada Software.
- MMVCServerSio is software that is issued from W-Okada Software, and can be downloaded here.
- Thats the actual software that does exports RVC models to ONNX files.
- Just load your model inside MMVCServerSio, and click on "Export ONNX":
-
- Enjoy you now have a ONNX model file for your voicepack! - - } />
- - Refers to this video and convert it to ONNX. - - } />
+ + + Here are some tutorials and guides about the Custom Voice Filter Plugin: + + {sections.map((section, index) => ( + + ))} + @@ -84,16 +94,22 @@ export function openWikiHomeModal(): string { return key; } -function CollapsibleCard({ title, content }: { title: string; content: React.ReactNode; }) { +interface CollapsibleCardProps { + title: string; + content: string; +} + +function CollapsibleCard({ title, content }: CollapsibleCardProps) { const [isOpen, setIsOpen] = useState(false); return ( - - setIsOpen(!isOpen)} style={{ cursor: "pointer", background: "var(--background-primary)", padding: "10px", marginBottom: isOpen ? "10px" : "0px" }}> - {title} + + setIsOpen(!isOpen)}> + {title} + - {isOpen && {content}} - + + ); } diff --git a/src/plugins/customVoiceFilter/index.tsx b/src/plugins/customVoiceFilter/index.tsx index 37d6eb5e1..aac7afd48 100644 --- a/src/plugins/customVoiceFilter/index.tsx +++ b/src/plugins/customVoiceFilter/index.tsx @@ -5,6 +5,8 @@ */ // Imports +import "./style.css"; + import { DataStore } from "@api/index"; import { Devs } from "@utils/constants"; import { proxyLazy } from "@utils/lazy"; @@ -17,7 +19,7 @@ import { openConfirmModal } from "./ConfirmModal"; import { openErrorModal } from "./ErrorModal"; import { CustomVoiceFilterChatBarIcon } from "./Icons"; import { downloadFile } from "./utils"; -export let voices: any = null; +export let voices: Record | null = null; export let VoiceFilterStyles: any = null; // still 'skye' export let VoiceFilterStore: any = null; @@ -223,10 +225,11 @@ export const useVoiceFiltersStore: ZustandStore = proxyL }; let i = 0; - for (const [, val] of Object.entries(voices) as [string, IVoiceFilter][]) { - if (!Object.values(voiceFilterState.voiceFilters).find(x => x.name === val.name)) - voiceFilterState.voiceFilters[++i] = { ...val, id: i, available: true, temporarilyAvailable: false }; - } + if (voices) + for (const [, val] of Object.entries(voices) as [string, IVoiceFilter][]) { + if (!Object.values(voiceFilterState.voiceFilters).find(x => x.name === val.name)) + voiceFilterState.voiceFilters[++i] = { ...val, id: i, available: true, temporarilyAvailable: false }; + } const { voiceFilters } = get(); Object.values(voiceFilters).forEach(voice => { @@ -304,8 +307,10 @@ export default definePlugin({ useVoiceFiltersStore.subscribe(store => store.updateVoicesList()); - const modulePath = await DiscordNative.fileManager.getModulePath(); - useVoiceFiltersStore.getState().modulePath = modulePath; + if (getClient().client === "desktop") { + const modulePath = await DiscordNative.fileManager.getModulePath(); + useVoiceFiltersStore.getState().modulePath = modulePath; + } // // ============ DEMO ============ // const templaceVoicePackObject: IVoiceFilter = JSON.parse(templateVoicepack); diff --git a/src/plugins/customVoiceFilter/style.css b/src/plugins/customVoiceFilter/style.css new file mode 100644 index 000000000..d62af8816 --- /dev/null +++ b/src/plugins/customVoiceFilter/style.css @@ -0,0 +1,77 @@ +.vc-voice-filters-wiki { + max-width: var(--modal-width-large); +} + +.vc-voice-filters-md { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.vc-voice-filters-md-image { + width: 100%; + max-height: 20rem; + overflow: hidden; + display: flex; + margin-block: 0.5rem; + + img { + width: 100%; + background: var(--background-tertiary); + border-radius: 0.5rem; + object-fit: contain; + } +} + +.vc-voice-filters-card { + width: 100%; + + .vc-voice-filters-card-title { + cursor: pointer; + background: var(--background-primary); + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + } + + .vc-voice-filters-card-icon { + transition: all 100ms ease-in-out; + } + + &.vc-voice-filters-card-open .vc-voice-filters-card-icon { + transform: rotate(180deg); + } +} + +.vc-voice-filters-modal-link { + border-radius: 3px; + padding: 0 2px; + font-weight: 500; + unicode-bidi: plaintext; + color: var(--mention-foreground); + background: var(--mention-background); + text-overflow: ellipsis; + overflow: hidden; +} + +.vc-voice-filters-details { + interpolate-size: allow-keywords; + transition: all 100ms ease-in-out; + overflow-y: hidden; + height: 0; + padding-inline: 1.2rem; + + .vc-voice-filters-card-open & { + height: auto; + } +} + +.vc-voice-filters-modal { + padding-block: 0.5rem; + color: var(--text-normal); +} + +.vc-voice-filters-voice-filter:not(.vc-voice-filters-voice-filter-available) img { + filter: brightness(0.5); +} diff --git a/src/plugins/customVoiceFilter/utils.ts b/src/plugins/customVoiceFilter/utils.ts index 764a65517..84095ea45 100644 --- a/src/plugins/customVoiceFilter/utils.ts +++ b/src/plugins/customVoiceFilter/utils.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { classNameFactory } from "@api/Styles"; import { useCallback, useEffect, useRef, useState } from "@webpack/common"; export function downloadFile(name: string, data: string): void { @@ -91,3 +92,5 @@ export function useAudio({ source, key = defaultKey }: UseAudioOptions = {}) { return { isPlaying, playSound, stopSound, preloadSound }; } + +export const cl = classNameFactory();