Compare commits
No commits in common. "main" and "4090-cuda-and-api" have entirely different histories.
main
...
4090-cuda-
293
README.md
293
README.md
@ -1,7 +1,292 @@
|
||||
# (QoL improvements for) TorToiSe
|
||||
# AI Voice Cloning for Retards and Savants
|
||||
|
||||
This repo is for my modifications to [neonbjb/tortoise-tts](https://github.com/neonbjb/tortoise-tts). If you need the original README, refer to the original repo.
|
||||
This [rentry](https://rentry.org/AI-Voice-Cloning/) aims to serve as both a foolproof guide for setting up AI voice cloning tools for legitimate, local use on Windows, as well as a stepping stone for anons that genuinely want to play around with [TorToiSe](https://github.com/neonbjb/tortoise-tts).
|
||||
|
||||
\> w-where'd everything go?
|
||||
Similar to my own findings for Stable Diffusion image generation, this rentry may appear a little disheveled as I note my new findings with TorToiSe. Please keep this in mind if the guide seems to shift a bit or sound confusing.
|
||||
|
||||
Please migrate to [mrq/ai-voice-cloning](https://git.ecker.tech/mrq/ai-voice-cloning), as that repo is the more cohesive package for voice cloning.
|
||||
>\>Ugh... why bother when I can just abuse 11.AI?
|
||||
|
||||
I very much encourage (You) to use 11.AI while it's still viable to use. For the layman, it's easier to go through the hoops of coughing up the $5 or abusing the free trial over actually setting up a TorToiSe environment and dealing with its quirks.
|
||||
|
||||
However, I also encourage your own experimentation with TorToiSe, as it's very, very promising, it just takes a little love and elbow grease.
|
||||
|
||||
## Glossary
|
||||
|
||||
To try and keep the terminology used here (somewhat) consistent and coherent, below are a list of terms, and their definitions (or at least, the way I'm using them):
|
||||
* `voice cloning`: synthesizing speech to accurately replicate a subject's voice.
|
||||
* `input clips` / `voice clips` / `audio input` / `voice samples` : the original voice source of the subject you're trying to clone.
|
||||
* `waveform`: the raw audio.
|
||||
* `sampling rate`: the bandwidth of a given waveform, represented as twice the frequency of the waveform it represents.
|
||||
* `voice latents` / `conditional latents` / `latents`: computated traits of a voice.
|
||||
* `autoregressive samples` (`samples` / `tokens`): the initial generation pass to output tokens, and (usually) the most computationally expensive. More samples = better "cloning".
|
||||
* `CLVP`: Contrastive Language-Voice Pretraining: an analog to CLIP, but for voices. After the autoregressive samples pass, those samples/tokens are compared against the CLVP to find the best candidates.
|
||||
* `CVVP`: Contrastive Voice-Voice Pretraining: a (deprecated) model that can be used weighted in junction with the CLVP.
|
||||
* `candidates`: results from the comparing against the CLVP/CVVP models. (Assumed to be) ordered from best to worst.
|
||||
* `diffusion decoder` / `vocoder`: these passes are responsible for encoding the tokens into a MEL spectrogram into a waveform.
|
||||
* `diffusion iterations`: how many passes to put into generating the output waveform. More iterations = better audio quality.
|
||||
* `diffusion sampler` / `sampler`: the sampling method used during the diffusion decoding pass, albeit a bit of a misnomer. Currently, only two samplers are implemented.
|
||||
|
||||
## Modifications
|
||||
|
||||
My fork boasts the following additions, fixes, and optimizations:
|
||||
* a competent web UI made in Gradio to expose a lot of tunables and options
|
||||
* cleaned up output structure of resulting audio files
|
||||
* caching computed conditional latents for faster re-runs
|
||||
- additionally, regenerating them if the script detects they're out of date
|
||||
* uses the entire audio sample instead of the first four seconds of each sound file for better reproducing
|
||||
* activated unused DDIM sampler
|
||||
* use of some optimizations like `kv_cache`ing for the autoregression sample pass, and keeping data on GPU
|
||||
* compatibilty with DirectML
|
||||
* easy install scripts
|
||||
* and more!
|
||||
|
||||
## Colab Notebook
|
||||
|
||||
A colab notebook to quickly set up and use this repo is included and available [here](https://colab.research.google.com/drive/1v2yTl-VkhYRflBUND-I9NuVWmCa5HeOg?usp=share_link): https://colab.research.google.com/drive/1v2yTl-VkhYRflBUND-I9NuVWmCa5HeOg
|
||||
|
||||
For the unfortunate using Paperspace, this notebook should also work for it.
|
||||
|
||||
## Installing
|
||||
|
||||
Outside of the very small prerequisites, everything needed to get TorToiSe working is included in the repo.
|
||||
|
||||
### Pre-Requirements
|
||||
|
||||
Windows:
|
||||
* Python 3.9: https://www.python.org/downloads/release/python-3913/
|
||||
* Git (optional): https://git-scm.com/download/win
|
||||
* CUDA drivers, if NVIDIA
|
||||
|
||||
Linux:
|
||||
* python3.x (tested with 3.10)
|
||||
* git
|
||||
* ROCm for AMD, CUDA for NVIDIA
|
||||
|
||||
### Setup
|
||||
|
||||
#### Windows
|
||||
|
||||
Download Python and Git and run their installers.
|
||||
|
||||
After installing Python, open the Start Menu and search for `Command Prompt`. Type `cd `, then drag and drop the folder you want to work in (experienced users can just `cd <path>` directly), then hit Enter.
|
||||
|
||||
Paste `git clone https://git.ecker.tech/mrq/tortoise-tts` to download TorToiSe and additional scripts, then hit Enter. Inexperienced users can just download the repo as a ZIP, and extract.
|
||||
|
||||
Afterwards, run the setup script, depending on your GPU, to automatically set things up.
|
||||
* AMD: `setup-directml.bat`
|
||||
* NVIDIA: `setup-cuda.bat`
|
||||
|
||||
If you've done everything right, you shouldn't have any errors.
|
||||
|
||||
##### Note on DirectML Support
|
||||
|
||||
PyTorch-DirectML is very, very experimental and is still not production quality. There's some headaches with the need for hairy kludgy patches.
|
||||
|
||||
These patches rely on transfering the tensor between the GPU and CPU as a hotfix, so performance is definitely harmed.
|
||||
|
||||
Both the conditional latent computation and the vocoder pass have to be done on the CPU entirely because of some quirks with DirectML.
|
||||
|
||||
On my 6800XT, VRAM usage climbs almost the entire 16GiB, so be wary if you OOM somehow. Low VRAM flags may NOT have any additional impact from the constant copying anyways.
|
||||
|
||||
For AMD users, I still might suggest using Linux+ROCm as it's (relatively) headache free, but I had stability problems.
|
||||
|
||||
#### Linux
|
||||
|
||||
First, make sure you have both `python3.x` and `git` installed, as well as the required compute platform according to your GPU (ROCm or CUDA).
|
||||
|
||||
Simply run the following block:
|
||||
|
||||
```
|
||||
git clone https://git.ecker.tech/mrq/tortoise-tts
|
||||
cd tortoise-tts
|
||||
chmod +x *.sh
|
||||
```
|
||||
|
||||
Then, depending on your GPU:
|
||||
* AMD: `./setup-rocm.sh`
|
||||
* NVIDIA: `./setup-cuda.sh`
|
||||
|
||||
And you should be done!
|
||||
|
||||
### Updating
|
||||
|
||||
To check for updates, simply run `update.bat` (or `update.sh`). It should pull from the repo, as well as fetch for any new dependencies.
|
||||
|
||||
### Pitfalls You May Encounter
|
||||
|
||||
I'll try and make a list of "common" (or what I feel may be common that I experience) issues with getting TorToiSe set up:
|
||||
* `CUDA is NOT available for use.`: If you're on Linux, you failed to set up CUDA (if NVIDIA) or ROCm (if AMD). Please make sure you have these installed on your system.
|
||||
If you're on Windows with an AMD card, you're stuck out of luck, as ROCm is not available on Windows (without major hoops to be jumped). If you're on an NVIDIA GPU, then I'm not sure what went wrong.
|
||||
* `failed reading zip archive: failed finding central directory`: You had a file fail to download completely during the model downloading initialization phase. Please open either `.\models\tortoise\` or `.\models\transformers\`, and delete the offending file.
|
||||
You can deduce what that file is by reading the stack trace. A few lines above the last like will be a line trying to read a model path.
|
||||
* `torch.cuda.OutOfMemoryError: CUDA out of memory.`: You most likely have a GPU with low VRAM (~4GiB), and the small optimizations with keeping data on the GPU is enough to OOM. Please open the `start.bat` file and add `--low-vram` to the command (for example: `py app.py --low-vram`) to disable those small optimizations.
|
||||
* `WavFileWarning: Chunk (non-data) not understood, skipping it.`: something about your WAVs are funny, and its best to remux your audio files with FFMPEG (included batch file in `.\convert\`).
|
||||
Honestly, I don't know if this does impact output quality, as I feel it's placebo when I do try and correct this.
|
||||
|
||||
## Preparing Voice Samples
|
||||
|
||||
Now that the tough part is dealt with, it's time to prepare voice clips to use.
|
||||
|
||||
Unlike training embeddings for AI image generations, preparing a "dataset" for voice cloning is very simple.
|
||||
|
||||
As a general rule of thumb, try to source clips that aren't noisy, solely the subject you are trying to clone, and doesn't contain any non-words (like yells, guttural noises, etc.). If you must, run your source through a background music/noise remover (how to is an exercise left to the reader). It isn't entirely a detriment if you're unable to provide clean audio, however. Just be wary that you might have some headaches with getting acceptable output.
|
||||
|
||||
Nine times out of ten, you should be fine using as many clips as possible. There's (now) no preference between combining your audio into one file, or leaving it split. However, if you're aiming for a specific delivery, it *should* be best for you to narrow down to just using that as your provided source (for example, changing one word in a line).
|
||||
|
||||
There's no hard specifics on how many, or how long, your sources should be.
|
||||
|
||||
If you're looking to trim your clips, in my opinion, ~~Audacity~~ Tenacity works good enough, as you can easily output your clips into the proper format (22050 Hz sampling rate).
|
||||
|
||||
Power users with FFMPEG already installed can simply used the provided conversion script in `.\convert\`.
|
||||
|
||||
After preparing your clips as WAV files at a sample rate of 22050 Hz, open up the `tortoise-tts` folder you're working in, navigate to the `voices` folder, create a new folder in whatever name you want, then dump your clips into that folder. While you're in the `voice` folder, you can take a look at the other provided voices.
|
||||
|
||||
**!**NOTE**!**: Before 2023.02.10, voices used to be stored under `.\tortoise\voices\`, but has been moved up one folder. Compatibily is maintained with the old voice folder, but will take priority.
|
||||
|
||||
## Using the Software
|
||||
|
||||
Now you're ready to generate clips. With the command prompt still open, simply enter `start.bat` (or `start.sh`), and wait for it to print out a URL to open in your browser, something like `http://127.0.0.1:7860`.
|
||||
|
||||
If you're looking to access your copy of TorToiSe from outside your local network, tick the `Public Share Gradio` button in the `Settings` tab, then restart.
|
||||
|
||||
### Generate
|
||||
|
||||
You'll be presented with a bunch of options in the default `Generate` tab, but do not be overwhelmed, as most of the defaults are sane, but below are a rough explanation on which input does what:
|
||||
* `Prompt`: text you want to be read. You wrap text in `[brackets]` for "prompt engineering", where it'll affect the output, but those words won't actually be read.
|
||||
* `Line Delimiter`: String to split the prompt into pieces. The stitched clip will be stored as `combined.wav`
|
||||
- Setting this to `\n` will generate each line as one clip before stitching it. Leave blank to disable this.
|
||||
* `Emotion`: the "emotion" used for the delivery. This is a shortcut to utilizing "prompt engineering" by starting with `[I am really <emotion>,]` in your prompt. This is merely a suggestion, not a guarantee.
|
||||
* `Custom Emotion + Prompt`: a non-preset "emotion" used for the delivery. This is a shortcut to utilizing "prompt engineering" by starting with `[<emotion>]` in your prompt.
|
||||
* `Voice`: the voice you want to clone. You can select `microphone` if you want to use input from your microphone.
|
||||
* `Microphone Source`: Use your own voice from a line-in source.
|
||||
* `Candidates`: number of outputs to generate, starting from the best candidate. Depending on your iteration steps, generating the final sound files could be cheap, but they only offer alternatives to the samples generated to pull from (in other words, the later candidates perform worse), so don't be compelled to generate a ton of candidates.
|
||||
* `Seed`: initializes the PRNG to this value. Use this if you want to reproduce a generated voice.
|
||||
* `Preset`: shortcut values for sample count and iteration steps. Clicking a preset will update its corresponding values. Higher presets result in better quality at the cost of computation time.
|
||||
* `Samples`: analogous to samples in image generation. More samples = better resemblance / clone quality, at the cost of performance. This strictly affects clone quality.
|
||||
* `Iterations`: influences audio sound quality in the final output. More iterations = higher quality sound. This step is relatively cheap, so do not be discouraged from increasing this. This strictly affects quality in the actual sound.
|
||||
* `Temperature`: how much randomness to introduce to the generated samples. Lower values = better resemblance to the source samples, but some temperature is still required for great output.
|
||||
- **!**NOTE**!**: This value is very inconsistent and entirely depends on the input voice. In other words, some voices will be receptive to playing with this value, while others won't make much of a difference.
|
||||
- **!**NOTE**!**: some voices will be very receptive to this, where it speaks slowly at low temperatures, but nudging it a hair and it speaks too fast.
|
||||
* `Pause Size`: Governs how large pauses are at the end of a clip (in token size, not seconds). Increase this if your output gets cut off at the end.
|
||||
- **!**NOTE**!**: too large of a pause size can lead to unexpected behavior.
|
||||
* `Diffusion Sampler`: sampler method during the diffusion pass. Currently, only `P` and `DDIM` are added, but does not seem to offer any substantial differences in my short tests.
|
||||
`P` refers to the default, vanilla sampling method in `diffusion.py`.
|
||||
To reiterate, this ***only*** is useful for the diffusion decoding path, after the autoregressive outputs are generated.
|
||||
|
||||
After you fill everything out, click `Run`, and wait for your output in the output window. The sampled voice is also returned, but if you're using multiple files, it'll return the first file, rather than a combined file.
|
||||
|
||||
All outputs are saved under `./result/[voice name]/[timestamp]/` as `result.wav`, and the settings in `input.txt`. There doesn't seem to be an inherent way to add a Download button in Gradio, so keep that folder in mind.
|
||||
|
||||
To save you from headaches, I strongly recommend playing around with shorter sentences first to find the right values for the voice you're using before generating longer sentences.
|
||||
|
||||
As a quick optimization, I modified the script to have the `conditional_latents` are saved after loading voice samples, and subsequent uses will load that file directly (at the cost of not returning the `Sample voice` to the web UI). Additionally, these `conditional_latents` are also computed in a way to use the entire clip, rather than the first four seconds the original tortoise-tts uses. If there's voice samples that have a modification time newer than this cached file, it'll skip loading it and load the normal WAVs instead.
|
||||
|
||||
**!**NOTE**!**: cached `latents.pth` files generated before 2023.02.05 will be ignored, due to a change in computing the conditiona latents. This *should* help bump up voice cloning quality. Apologies for the inconvenience.
|
||||
|
||||
### History
|
||||
|
||||
In this tab, a rudimentary way of viewing past results can be found here.
|
||||
|
||||
With it, you just select a voice, then you can quickly view their generation settings.
|
||||
|
||||
To play a file, select a specific file with the second dropdown list.
|
||||
|
||||
To reuse a voice file's settings, click `Copy Settings`.
|
||||
|
||||
### Utilities
|
||||
|
||||
In this tab, you can find some helper utilities that might be of assistance.
|
||||
|
||||
For now, an analog to the PNG info found in Voldy's Stable Diffusion Web UI resides here. With it, you can upload an audio file generated with this web UI to view the settings used to generate that output. Additionally, the voice latents used to generate the uploaded audio clip can be extracted.
|
||||
|
||||
If you want to reuse its generation settings, simply click `Copy Settings`.
|
||||
|
||||
To import a voice, click `Import Voice`. Remember to click `Refresh Voice List` in the `Generate` panel afterwards, if it's a new voice.
|
||||
|
||||
### Settings
|
||||
|
||||
This tab (should) hold a bunch of other settings, from tunables that shouldn't be tampered with, to settings pertaining to the web UI itself.
|
||||
|
||||
Below are settings that override the default launch arguments. Some of these require restarting to work.
|
||||
* `Listen`: sets the hostname, port, and/or path for the web UI to listen on.
|
||||
- For example, `0.0.0.0:80` will have the web UI accept all connections on port 80
|
||||
- For example, `10.0.0.1:8008/gradio` will have the web UI only accept connections through `10.0.0.1`, at the path `/gradio`
|
||||
* `Public Share Gradio`: Tells Gradio to generate a public URL for the web UI. Ignored if specifying a path through the `Listen` setting.
|
||||
* `Check for Updates`: checks for updates on page load and notifies in console. Only works if you pulled this repo from a gitea instance.
|
||||
* `Only Load Models Locally`: enforces offline mode for loading models. This is the equivalent of setting the env var: `TRANSFORMERS_OFFLINE`
|
||||
* `Low VRAM`: disables optimizations in TorToiSe that increases VRAM consumption. Suggested if your GPU has under 6GiB.
|
||||
* `Embed Output Metadata`: enables embedding the settings and latents used to generate that audio clip inside that audio clip. Metadata is stored as a JSON string in the `lyrics` tag.
|
||||
* `Slimmer Computed Latents`: falls back to the original, 12.9KiB way of storing latents (without the extra bits required for using the CVVP model).
|
||||
* `Voice Latent Max Chunk Size`: during the voice latents calculation pass, this limits how large, in bytes, a chunk can be. Large values can run into VRAM OOM errors.
|
||||
* `Sample Batch Size`: sets the batch size when generating autoregressive samples. Bigger batches result in faster compute, at the cost of increased VRAM consumption. Leave to 0 to calculate a "best" fit.
|
||||
* `Concurrency Count`: how many Gradio events the queue can process at once. Leave this over 1 if you want to modify settings in the UI that updates other settings while generating audio clips.
|
||||
* `Output Sample Rate`: the sample rate to save the generated audio as. It provides a bit of slight bump in quality
|
||||
* `Output Volume`: adjusts the volume through amplitude scaling
|
||||
|
||||
Below are an explanation of experimental flags. Messing with these might impact performance, as these are exposed only if you know what you are doing.
|
||||
* `Half-Precision`: (attempts to) hint to PyTorch to auto-cast to float16 (half precision) for compute. Disabled by default, due to it making computations slower.
|
||||
* `Conditional Free`: a quality boosting improvement at the cost of some performance. Enabled by default, as I think the penaly is negligible in the end.
|
||||
* `CVVP Weight`: governs how much weight the CVVP model should influence candidates. The original documentation mentions this is deprecated as it does not really influence things, but you're still free to play around with it.
|
||||
Currently, setting requires regenerating your voice latents, as I forgot to have it return some extra data that weighing against the CVVP model uses. Oops.
|
||||
Setting this to 1 leads to bad behavior.
|
||||
* `Top P`: P value used in nucleus sampling; lower values mean the decoder produces more "likely" (aka boring) outputs.
|
||||
* `Diffusion Temperature`: the variance of the noise fed into the diffusion model; values at 0 are the "mean" prediction of the diffusion network and will sound bland and smeared.
|
||||
* `Length Penalty`: a length penalty applied to the autoregressive decoder; higher settings causes the model to produce more terse outputs.
|
||||
* `Repetition Penalty`: a penalty that prevents the autoregressive decoder from repeating itself during decoding. Can be used to reduce the incidence of long silences or "uhhhhhhs", etc.
|
||||
* `Conditioning-Free K`: determintes balancing the conditioning free signal with the conditioning-present signal.
|
||||
|
||||
## Example(s)
|
||||
|
||||
Below are some (rather outdated) outputs I deem substantial enough to share. As I continue delving into TorToiSe, I'll supply more examples and the values I use.
|
||||
|
||||
Source (Patrick Bateman):
|
||||
* https://files.catbox.moe/skzumo.zip
|
||||
|
||||
Output (`My name is Patrick Bateman.`, `fast` preset):
|
||||
* https://files.catbox.moe/cw88t5.wav
|
||||
* https://files.catbox.moe/bwunfo.wav
|
||||
* https://files.catbox.moe/ppxprv.wav
|
||||
|
||||
I trimmed up some of the samples to end up with ten short clips of about 10 seconds each. With a 2060, it took a hair over a minute to generate the initial samples, then five to ten seconds for each clip of a total of three. Not too bad for something running on consumer grade shitware.
|
||||
|
||||
Source (Harry Mason):
|
||||
* https://files.catbox.moe/n2xor1.mp3
|
||||
* https://files.catbox.moe/bbfke3.mp3
|
||||
|
||||
Output (The McDonalds building creepypasta, custom preset of 128 samples, 256 iterations):
|
||||
* https://voca.ro/16XSgdlcC5uT
|
||||
|
||||
This took quite a while, over the course of a day half-paying-attention at the command prompt to generate the next piece. I only had to regenerate one section that sounded funny, but compared to 11.AI requiring tons of regenerations for something usable, this is nice to just let run and forget. Initially he sounds rather passable as Harry Mason, but as it goes on it seems to kinda falter. Sound effects and music are added in post and aren't generated by TorToiSe.
|
||||
|
||||
Source (James Sunderland):
|
||||
* https://files.catbox.moe/ynoeld.mp3
|
||||
* https://files.catbox.moe/lxgbsm.mp3
|
||||
|
||||
Output (The McDonalds building creepypasta, 256 samples, 256 iterations, 0.1 temp, pause size 8, DDIM, conditioning free, seed 1675690127):
|
||||
* https://vocaroo.com/1nXmip0oJu8Z
|
||||
|
||||
This took a while to generate while I slept (and even managed to wake up before it finished). Using the batch function, this took 6.919 hours on my 2060 to generate the 27 pieces with zero editing on my end.
|
||||
|
||||
I'm providing this even with its nasty warts to highlight the quirks: the weird gaps where there's a strange sound instead, the random pauses for "thought", etc.
|
||||
|
||||
I think this also highlights how just combining your entire source sample gung-ho isn't a good idea, as he's not as high of a pitch in his delivery compared to how he usually is throughout most of the game (a sort of average between his two ranges). I can't gauge how well it did in reproducing it, since my ears are pretty much burnt out from listening to so many clips, but I believe he's pretty believable as a James Sunderland.
|
||||
|
||||
Output (`Is that really you, Mary?`, Ultra Fast preset, settings and latents embedded)
|
||||
* https://files.catbox.moe/gy1jvz.wav
|
||||
|
||||
This was just a quick test for an adjustable setting, but this one turned out really nice (for being a quick test) on the off chance. It's not the original delivery, and it definitely sounds robotic still, but it's on the Ultra Fast preset, as expected.
|
||||
|
||||
## Caveats (and Upsides)
|
||||
|
||||
To me, I find a few problems with TorToiSe over 11.AI:
|
||||
* computation time is quite an issue. Despite Stable Diffusion proving to be adequate on my 2060, TorToiSe takes quite some time with modest settings.
|
||||
- However, on my 6800XT, performance was drastically uplifted due to having more VRAM for larger batch sizes (at the cost of Krashing).
|
||||
* reproducability in a voice depends on the "compatibilty" with the model TorToiSe was trained on.
|
||||
- However, this also appears to be similar to 11.AI, where it was mostly trained on audiobook readings.
|
||||
* the lack of an obvious analog to the "stability" and "similarity" sliders kind of sucks, but it's not the end of the world.
|
||||
However, the `temperature` option seems to prove to be a proper analog to either of these.
|
||||
|
||||
Although, I can look past these as TorToiSe offers, in comparison to 11.AI:
|
||||
* the "speaking too fast" issue does not exist with TorToiSe. I don't need to fight with it by pretending I'm a Gaia user in the early 2000s by sprinkling ellipses.
|
||||
* the overall delivery seems very natural, sometimes small, dramatic pauses gets added at the legitimately most convenient moments, and the inhales tend to be more natural. Many of vocaroos from 11.AI where it just does not seem properly delivered.
|
||||
* being able to run it locally means I do not have to worry about some Polack seeing me use the "dick" word.
|
||||
283
README_OLD.md
Executable file
283
README_OLD.md
Executable file
@ -0,0 +1,283 @@
|
||||
# TorToiSe
|
||||
|
||||
Tortoise is a text-to-speech program built with the following priorities:
|
||||
|
||||
1. Strong multi-voice capabilities.
|
||||
2. Highly realistic prosody and intonation.
|
||||
|
||||
This repo contains all the code needed to run Tortoise TTS in inference mode.
|
||||
|
||||
A (*very*) rough draft of the Tortoise paper is now available in doc format. I would definitely appreciate any comments, suggestions or reviews:
|
||||
https://docs.google.com/document/d/13O_eyY65i6AkNrN_LdPhpUjGhyTNKYHvDrIvHnHe1GA
|
||||
|
||||
### Version history
|
||||
|
||||
#### v2.4; 2022/5/17
|
||||
- Removed CVVP model. Found that it does not, in fact, make an appreciable difference in the output.
|
||||
- Add better debugging support; existing tools now spit out debug files which can be used to reproduce bad runs.
|
||||
|
||||
#### v2.3; 2022/5/12
|
||||
- New CLVP-large model for further improved decoding guidance.
|
||||
- Improvements to read.py and do_tts.py (new options)
|
||||
|
||||
#### v2.2; 2022/5/5
|
||||
- Added several new voices from the training set.
|
||||
- Automated redaction. Wrap the text you want to use to prompt the model but not be spoken in brackets.
|
||||
- Bug fixes
|
||||
|
||||
#### v2.1; 2022/5/2
|
||||
- Added ability to produce totally random voices.
|
||||
- Added ability to download voice conditioning latent via a script, and then use a user-provided conditioning latent.
|
||||
- Added ability to use your own pretrained models.
|
||||
- Refactored directory structures.
|
||||
- Performance improvements & bug fixes.
|
||||
|
||||
## What's in a name?
|
||||
|
||||
I'm naming my speech-related repos after Mojave desert flora and fauna. Tortoise is a bit tongue in cheek: this model
|
||||
is insanely slow. It leverages both an autoregressive decoder **and** a diffusion decoder; both known for their low
|
||||
sampling rates. On a K80, expect to generate a medium sized sentence every 2 minutes.
|
||||
|
||||
## Demos
|
||||
|
||||
See [this page](http://nonint.com/static/tortoise_v2_examples.html) for a large list of example outputs.
|
||||
|
||||
Cool application of Tortoise+GPT-3 (not by me): https://twitter.com/lexman_ai
|
||||
|
||||
## Usage guide
|
||||
|
||||
### Colab
|
||||
|
||||
Colab is the easiest way to try this out. I've put together a notebook you can use here:
|
||||
https://colab.research.google.com/drive/1wVVqUPqwiDBUVeWWOUNglpGhU3hg_cbR?usp=sharing
|
||||
|
||||
### Local Installation
|
||||
|
||||
If you want to use this on your own computer, you must have an NVIDIA GPU.
|
||||
|
||||
First, install pytorch using these instructions: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/).
|
||||
On Windows, I **highly** recommend using the Conda installation path. I have been told that if you do not do this, you
|
||||
will spend a lot of time chasing dependency problems.
|
||||
|
||||
Next, install TorToiSe and it's dependencies:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/neonbjb/tortoise-tts.git
|
||||
cd tortoise-tts
|
||||
python -m pip install -r ./requirements.txt
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
If you are on windows, you will also need to install pysoundfile: `conda install -c conda-forge pysoundfile`
|
||||
|
||||
### do_tts.py
|
||||
|
||||
This script allows you to speak a single phrase with one or more voices.
|
||||
```shell
|
||||
python tortoise/do_tts.py --text "I'm going to speak this" --voice random --preset fast
|
||||
```
|
||||
|
||||
### read.py
|
||||
|
||||
This script provides tools for reading large amounts of text.
|
||||
|
||||
```shell
|
||||
python tortoise/read.py --textfile <your text to be read> --voice random
|
||||
```
|
||||
|
||||
This will break up the textfile into sentences, and then convert them to speech one at a time. It will output a series
|
||||
of spoken clips as they are generated. Once all the clips are generated, it will combine them into a single file and
|
||||
output that as well.
|
||||
|
||||
Sometimes Tortoise screws up an output. You can re-generate any bad clips by re-running `read.py` with the --regenerate
|
||||
argument.
|
||||
|
||||
### API
|
||||
|
||||
Tortoise can be used programmatically, like so:
|
||||
|
||||
```python
|
||||
reference_clips = [utils.audio.load_audio(p, 22050) for p in clips_paths]
|
||||
tts = api.TextToSpeech()
|
||||
pcm_audio = tts.tts_with_preset("your text here", voice_samples=reference_clips, preset='fast')
|
||||
```
|
||||
|
||||
## Voice customization guide
|
||||
|
||||
Tortoise was specifically trained to be a multi-speaker model. It accomplishes this by consulting reference clips.
|
||||
|
||||
These reference clips are recordings of a speaker that you provide to guide speech generation. These clips are used to determine many properties of the output, such as the pitch and tone of the voice, speaking speed, and even speaking defects like a lisp or stuttering. The reference clip is also used to determine non-voice related aspects of the audio output like volume, background noise, recording quality and reverb.
|
||||
|
||||
### Random voice
|
||||
|
||||
I've included a feature which randomly generates a voice. These voices don't actually exist and will be random every time you run
|
||||
it. The results are quite fascinating and I recommend you play around with it!
|
||||
|
||||
You can use the random voice by passing in 'random' as the voice name. Tortoise will take care of the rest.
|
||||
|
||||
For the those in the ML space: this is created by projecting a random vector onto the voice conditioning latent space.
|
||||
|
||||
### Provided voices
|
||||
|
||||
This repo comes with several pre-packaged voices. Voices prepended with "train_" came from the training set and perform
|
||||
far better than the others. If your goal is high quality speech, I recommend you pick one of them. If you want to see
|
||||
what Tortoise can do for zero-shot mimicking, take a look at the others.
|
||||
|
||||
### Adding a new voice
|
||||
|
||||
To add new voices to Tortoise, you will need to do the following:
|
||||
|
||||
1. Gather audio clips of your speaker(s). Good sources are YouTube interviews (you can use youtube-dl to fetch the audio), audiobooks or podcasts. Guidelines for good clips are in the next section.
|
||||
2. Cut your clips into ~10 second segments. You want at least 3 clips. More is better, but I only experimented with up to 5 in my testing.
|
||||
3. Save the clips as a WAV file with floating point format and a 22,050 sample rate.
|
||||
4. Create a subdirectory in voices/
|
||||
5. Put your clips in that subdirectory.
|
||||
6. Run tortoise utilities with --voice=<your_subdirectory_name>.
|
||||
|
||||
### Picking good reference clips
|
||||
|
||||
As mentioned above, your reference clips have a profound impact on the output of Tortoise. Following are some tips for picking
|
||||
good clips:
|
||||
|
||||
1. Avoid clips with background music, noise or reverb. These clips were removed from the training dataset. Tortoise is unlikely to do well with them.
|
||||
2. Avoid speeches. These generally have distortion caused by the amplification system.
|
||||
3. Avoid clips from phone calls.
|
||||
4. Avoid clips that have excessive stuttering, stammering or words like "uh" or "like" in them.
|
||||
5. Try to find clips that are spoken in such a way as you wish your output to sound like. For example, if you want to hear your target voice read an audiobook, try to find clips of them reading a book.
|
||||
6. The text being spoken in the clips does not matter, but diverse text does seem to perform better.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Generation settings
|
||||
|
||||
Tortoise is primarily an autoregressive decoder model combined with a diffusion model. Both of these have a lot of knobs
|
||||
that can be turned that I've abstracted away for the sake of ease of use. I did this by generating thousands of clips using
|
||||
various permutations of the settings and using a metric for voice realism and intelligibility to measure their effects. I've
|
||||
set the defaults to the best overall settings I was able to find. For specific use-cases, it might be effective to play with
|
||||
these settings (and it's very likely that I missed something!)
|
||||
|
||||
These settings are not available in the normal scripts packaged with Tortoise. They are available, however, in the API. See
|
||||
```api.tts``` for a full list.
|
||||
|
||||
### Prompt engineering
|
||||
|
||||
Some people have discovered that it is possible to do prompt engineering with Tortoise! For example, you can evoke emotion
|
||||
by including things like "I am really sad," before your text. I've built an automated redaction system that you can use to
|
||||
take advantage of this. It works by attempting to redact any text in the prompt surrounded by brackets. For example, the
|
||||
prompt "\[I am really sad,\] Please feed me." will only speak the words "Please feed me" (with a sad tonality).
|
||||
|
||||
### Playing with the voice latent
|
||||
|
||||
Tortoise ingests reference clips by feeding them through individually through a small submodel that produces a point latent,
|
||||
then taking the mean of all of the produced latents. The experimentation I have done has indicated that these point latents
|
||||
are quite expressive, affecting everything from tone to speaking rate to speech abnormalities.
|
||||
|
||||
This lends itself to some neat tricks. For example, you can combine feed two different voices to tortoise and it will output
|
||||
what it thinks the "average" of those two voices sounds like.
|
||||
|
||||
#### Generating conditioning latents from voices
|
||||
|
||||
Use the script `get_conditioning_latents.py` to extract conditioning latents for a voice you have installed. This script
|
||||
will dump the latents to a .pth pickle file. The file will contain a single tuple, (autoregressive_latent, diffusion_latent).
|
||||
|
||||
Alternatively, use the api.TextToSpeech.get_conditioning_latents() to fetch the latents.
|
||||
|
||||
#### Using raw conditioning latents to generate speech
|
||||
|
||||
After you've played with them, you can use them to generate speech by creating a subdirectory in voices/ with a single
|
||||
".pth" file containing the pickled conditioning latents as a tuple (autoregressive_latent, diffusion_latent).
|
||||
|
||||
### Send me feedback!
|
||||
|
||||
Probabilistic models like Tortoise are best thought of as an "augmented search" - in this case, through the space of possible
|
||||
utterances of a specific string of text. The impact of community involvement in perusing these spaces (such as is being done with
|
||||
GPT-3 or CLIP) has really surprised me. If you find something neat that you can do with Tortoise that isn't documented here,
|
||||
please report it to me! I would be glad to publish it to this page.
|
||||
|
||||
## Tortoise-detect
|
||||
|
||||
Out of concerns that this model might be misused, I've built a classifier that tells the likelihood that an audio clip
|
||||
came from Tortoise.
|
||||
|
||||
This classifier can be run on any computer, usage is as follows:
|
||||
|
||||
```commandline
|
||||
python tortoise/is_this_from_tortoise.py --clip=<path_to_suspicious_audio_file>
|
||||
```
|
||||
|
||||
This model has 100% accuracy on the contents of the results/ and voices/ folders in this repo. Still, treat this classifier
|
||||
as a "strong signal". Classifiers can be fooled and it is likewise not impossible for this classifier to exhibit false
|
||||
positives.
|
||||
|
||||
## Model architecture
|
||||
|
||||
Tortoise TTS is inspired by OpenAI's DALLE, applied to speech data and using a better decoder. It is made up of 5 separate
|
||||
models that work together. I've assembled a write-up of the system architecture here:
|
||||
[https://nonint.com/2022/04/25/tortoise-architectural-design-doc/](https://nonint.com/2022/04/25/tortoise-architectural-design-doc/)
|
||||
|
||||
## Training
|
||||
|
||||
These models were trained on my "homelab" server with 8 RTX 3090s over the course of several months. They were trained on a dataset consisting of
|
||||
~50k hours of speech data, most of which was transcribed by [ocotillo](http://www.github.com/neonbjb/ocotillo). Training was done on my own
|
||||
[DLAS](https://github.com/neonbjb/DL-Art-School) trainer.
|
||||
|
||||
I currently do not have plans to release the training configurations or methodology. See the next section..
|
||||
|
||||
## Ethical Considerations
|
||||
|
||||
Tortoise v2 works considerably better than I had planned. When I began hearing some of the outputs of the last few versions, I began
|
||||
wondering whether or not I had an ethically unsound project on my hands. The ways in which a voice-cloning text-to-speech system
|
||||
could be misused are many. It doesn't take much creativity to think up how.
|
||||
|
||||
After some thought, I have decided to go forward with releasing this. Following are the reasons for this choice:
|
||||
|
||||
1. It is primarily good at reading books and speaking poetry. Other forms of speech do not work well.
|
||||
2. It was trained on a dataset which does not have the voices of public figures. While it will attempt to mimic these voices if they are provided as references, it does not do so in such a way that most humans would be fooled.
|
||||
3. The above points could likely be resolved by scaling up the model and the dataset. For this reason, I am currently withholding details on how I trained the model, pending community feedback.
|
||||
4. I am releasing a separate classifier model which will tell you whether a given audio clip was generated by Tortoise or not. See `tortoise-detect` above.
|
||||
5. If I, a tinkerer with a BS in computer science with a ~$15k computer can build this, then any motivated corporation or state can as well. I would prefer that it be in the open and everyone know the kinds of things ML can do.
|
||||
|
||||
### Diversity
|
||||
|
||||
The diversity expressed by ML models is strongly tied to the datasets they were trained on.
|
||||
|
||||
Tortoise was trained primarily on a dataset consisting of audiobooks. I made no effort to
|
||||
balance diversity in this dataset. For this reason, Tortoise will be particularly poor at generating the voices of minorities
|
||||
or of people who speak with strong accents.
|
||||
|
||||
## Looking forward
|
||||
|
||||
Tortoise v2 is about as good as I think I can do in the TTS world with the resources I have access to. A phenomenon that happens when
|
||||
training very large models is that as parameter count increases, the communication bandwidth needed to support distributed training
|
||||
of the model increases multiplicatively. On enterprise-grade hardware, this is not an issue: GPUs are attached together with
|
||||
exceptionally wide buses that can accommodate this bandwidth. I cannot afford enterprise hardware, though, so I am stuck.
|
||||
|
||||
I want to mention here
|
||||
that I think Tortoise could do be a **lot** better. The three major components of Tortoise are either vanilla Transformer Encoder stacks
|
||||
or Decoder stacks. Both of these types of models have a rich experimental history with scaling in the NLP realm. I see no reason
|
||||
to believe that the same is not true of TTS.
|
||||
|
||||
The largest model in Tortoise v2 is considerably smaller than GPT-2 large. It is 20x smaller that the original DALLE transformer.
|
||||
Imagine what a TTS model trained at or near GPT-3 or DALLE scale could achieve.
|
||||
|
||||
If you are an ethical organization with computational resources to spare interested in seeing what this model could do
|
||||
if properly scaled out, please reach out to me! I would love to collaborate on this.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This project has garnered more praise than I expected. I am standing on the shoulders of giants, though, and I want to
|
||||
credit a few of the amazing folks in the community that have helped make this happen:
|
||||
|
||||
- Hugging Face, who wrote the GPT model and the generate API used by Tortoise, and who hosts the model weights.
|
||||
- [Ramesh et al](https://arxiv.org/pdf/2102.12092.pdf) who authored the DALLE paper, which is the inspiration behind Tortoise.
|
||||
- [Nichol and Dhariwal](https://arxiv.org/pdf/2102.09672.pdf) who authored the (revision of) the code that drives the diffusion model.
|
||||
- [Jang et al](https://arxiv.org/pdf/2106.07889.pdf) who developed and open-sourced univnet, the vocoder this repo uses.
|
||||
- [Kim and Jung](https://github.com/mindslab-ai/univnet) who implemented univnet pytorch model.
|
||||
- [lucidrains](https://github.com/lucidrains) who writes awesome open source pytorch models, many of which are used here.
|
||||
- [Patrick von Platen](https://huggingface.co/patrickvonplaten) whose guides on setting up wav2vec were invaluable to building my dataset.
|
||||
|
||||
## Notice
|
||||
|
||||
Tortoise was built entirely by me using my own hardware. My employer was not involved in any facet of Tortoise's development.
|
||||
|
||||
If you use this repo or the ideas therein for your research, please cite it! A bibtex entree can be found in the right pane on GitHub.
|
||||
@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
rm .\in\.gitkeep
|
||||
rm .\out\.gitkeep
|
||||
for %%a in (".\in\*.*") do ffmpeg -i "%%a" -ac 1 ".\out\%%~na.wav"
|
||||
for %%a in (".\in\*.*") do ffmpeg -i "%%a" -ar 22050 -ac 1 -c:a pcm_f32le ".\out\%%~na.wav"
|
||||
2
convert/convert.sh
Executable file → Normal file
2
convert/convert.sh
Executable file → Normal file
@ -1 +1 @@
|
||||
for a in $(find "in/" -maxdepth 1 -not -name '.gitkeep' -type f); do ffmpeg -i "$a" -ac 1 "out/$(basename $a).wav"; done
|
||||
for a in $(find "in/" -maxdepth 1 -not -name '.gitkeep' -type f); do ffmpeg -i "$a" -ar 22050 -ac 1 -c:a pcm_f32le "out/$(basename $a).wav"; done
|
||||
27
main.py
Executable file
27
main.py
Executable file
@ -0,0 +1,27 @@
|
||||
import webui as mrq
|
||||
|
||||
if __name__ == "__main__":
|
||||
mrq.args = mrq.setup_args()
|
||||
|
||||
if mrq.args.listen_path is not None and mrq.args.listen_path != "/":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host=mrq.args.listen_host, port=mrq.args.listen_port if not None else 8000)
|
||||
else:
|
||||
mrq.webui = mrq.setup_gradio()
|
||||
mrq.webui.launch(share=mrq.args.share, prevent_thread_lock=True, server_name=mrq.args.listen_host, server_port=mrq.args.listen_port)
|
||||
mrq.tts = mrq.setup_tortoise()
|
||||
|
||||
mrq.webui.block_thread()
|
||||
elif __name__ == "main":
|
||||
from fastapi import FastAPI
|
||||
import gradio as gr
|
||||
|
||||
import sys
|
||||
sys.argv = [sys.argv[0]]
|
||||
|
||||
app = FastAPI()
|
||||
mrq.args = mrq.setup_args()
|
||||
mrq.webui = mrq.setup_gradio()
|
||||
app = gr.mount_gradio_app(app, mrq.webui, path=mrq.args.listen_path)
|
||||
|
||||
mrq.tts = mrq.setup_tortoise()
|
||||
@ -7,9 +7,12 @@ progressbar
|
||||
einops
|
||||
unidecode
|
||||
scipy
|
||||
librosa==0.8.1
|
||||
librosa
|
||||
torchaudio
|
||||
threadpoolctl
|
||||
appdirs
|
||||
numpy<=1.23.5
|
||||
numba
|
||||
numpy
|
||||
numba
|
||||
gradio
|
||||
music-tag
|
||||
k-diffusion
|
||||
@ -1,4 +0,0 @@
|
||||
gradio
|
||||
music-tag
|
||||
k-diffusion
|
||||
voicefixer
|
||||
10
setup-cuda.bat
Executable file
10
setup-cuda.bat
Executable file
@ -0,0 +1,10 @@
|
||||
python -m venv tortoise-venv
|
||||
call .\tortoise-venv\Scripts\activate.bat
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
python -m pip install --pre torchvision --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
python -m pip install --pre torchaudio --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
python -m pip install -r ./requirements.txt
|
||||
python setup.py install
|
||||
deactivate
|
||||
pause
|
||||
10
setup-cuda.sh
Normal file
10
setup-cuda.sh
Normal file
@ -0,0 +1,10 @@
|
||||
python -m venv tortoise-venv
|
||||
source ./tortoise-venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
# CUDA
|
||||
pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
pip install --pre torchvision --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
pip install --pre torchaudio --index-url https://download.pytorch.org/whl/nightly/cu118
|
||||
python -m pip install -r ./requirements.txt
|
||||
python setup.py install
|
||||
deactivate
|
||||
8
setup-directml.bat
Executable file
8
setup-directml.bat
Executable file
@ -0,0 +1,8 @@
|
||||
python -m venv tortoise-venv
|
||||
call .\tortoise-venv\Scripts\activate.bat
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install torch torchvision torchaudio torch-directml==0.1.13.1.dev230119
|
||||
python -m pip install -r ./requirements.txt
|
||||
python setup.py install
|
||||
deactivate
|
||||
pause
|
||||
8
setup-rocm.sh
Normal file
8
setup-rocm.sh
Normal file
@ -0,0 +1,8 @@
|
||||
python -m venv tortoise-venv
|
||||
source ./tortoise-venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
# ROCM
|
||||
pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/rocm5.1.1 # 5.2 does not work for me desu
|
||||
python -m pip install -r ./requirements.txt
|
||||
python setup.py install
|
||||
deactivate
|
||||
10
setup.py
10
setup.py
@ -6,13 +6,13 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
||||
setuptools.setup(
|
||||
name="TorToiSe",
|
||||
packages=setuptools.find_packages(),
|
||||
version="2.4.5",
|
||||
version="2.4.2",
|
||||
author="James Betker",
|
||||
author_email="james@adamant.ai",
|
||||
description="A high quality multi-voice text-to-speech library",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://git.ecker.tech/mrq/tortoise-tts",
|
||||
url="https://github.com/neonbjb/tortoise-tts",
|
||||
project_urls={},
|
||||
scripts=[
|
||||
'scripts/tortoise_tts.py',
|
||||
@ -29,12 +29,6 @@ setuptools.setup(
|
||||
'librosa',
|
||||
'transformers',
|
||||
'tokenizers',
|
||||
'transformers==4.19',
|
||||
'torchaudio',
|
||||
'threadpoolctl',
|
||||
'appdirs',
|
||||
'numpy',
|
||||
'numba',
|
||||
],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
|
||||
4
start.bat
Executable file
4
start.bat
Executable file
@ -0,0 +1,4 @@
|
||||
call .\tortoise-venv\Scripts\activate.bat
|
||||
python main.py
|
||||
deactivate
|
||||
pause
|
||||
3
start.sh
Executable file
3
start.sh
Executable file
@ -0,0 +1,3 @@
|
||||
source ./tortoise-venv/bin/activate
|
||||
python3 ./main.py
|
||||
deactivate
|
||||
585
tortoise/api.py
585
tortoise/api.py
@ -5,7 +5,12 @@ import gc
|
||||
|
||||
from time import time
|
||||
from urllib import request
|
||||
from urllib.request import ProxyHandler, build_opener, install_opener
|
||||
|
||||
if 'TORTOISE_MODELS_DIR' not in os.environ:
|
||||
os.environ['TORTOISE_MODELS_DIR'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../models/tortoise/')
|
||||
|
||||
if 'TRANSFORMERS_CACHE' not in os.environ:
|
||||
os.environ['TRANSFORMERS_CACHE'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../models/transformers/')
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
@ -22,18 +27,18 @@ from tortoise.models.clvp import CLVP
|
||||
from tortoise.models.cvvp import CVVP
|
||||
from tortoise.models.random_latent_generator import RandomLatentConverter
|
||||
from tortoise.models.vocoder import UnivNetGenerator
|
||||
from tortoise.models.bigvgan import BigVGAN
|
||||
|
||||
from tortoise.utils.audio import wav_to_univnet_mel, denormalize_tacotron_mel
|
||||
from tortoise.utils.diffusion import SpacedDiffusion, space_timesteps, get_named_beta_schedule
|
||||
from tortoise.utils.tokenizer import VoiceBpeTokenizer
|
||||
from tortoise.utils.wav2vec_alignment import Wav2VecAlignment
|
||||
|
||||
from tortoise.utils.device import get_device, get_device_name, get_device_batch_size, print_stats, do_gc
|
||||
from tortoise.utils.device import get_device, get_device_name, get_device_batch_size
|
||||
|
||||
pbar = None
|
||||
|
||||
STOP_SIGNAL = False
|
||||
MODELS_DIR = os.environ.get('TORTOISE_MODELS_DIR', os.path.realpath(os.path.join(os.getcwd(), './models/tortoise/')))
|
||||
|
||||
MODELS_DIR = os.environ.get('TORTOISE_MODELS_DIR')
|
||||
MODELS = {
|
||||
'autoregressive.pth': 'https://huggingface.co/jbetker/tortoise-tts-v2/resolve/main/.models/autoregressive.pth',
|
||||
'classifier.pth': 'https://huggingface.co/jbetker/tortoise-tts-v2/resolve/main/.models/classifier.pth',
|
||||
@ -43,46 +48,21 @@ MODELS = {
|
||||
'vocoder.pth': 'https://huggingface.co/jbetker/tortoise-tts-v2/resolve/main/.models/vocoder.pth',
|
||||
'rlg_auto.pth': 'https://huggingface.co/jbetker/tortoise-tts-v2/resolve/main/.models/rlg_auto.pth',
|
||||
'rlg_diffuser.pth': 'https://huggingface.co/jbetker/tortoise-tts-v2/resolve/main/.models/rlg_diffuser.pth',
|
||||
|
||||
'bigvgan_base_24khz_100band.pth': 'https://huggingface.co/ecker/tortoise-tts-models/resolve/main/models/bigvgan_base_24khz_100band.pth',
|
||||
'bigvgan_24khz_100band.pth': 'https://huggingface.co/ecker/tortoise-tts-models/resolve/main/models/bigvgan_24khz_100band.pth',
|
||||
|
||||
'bigvgan_base_24khz_100band.json': 'https://huggingface.co/ecker/tortoise-tts-models/resolve/main/models/bigvgan_base_24khz_100band.json',
|
||||
'bigvgan_24khz_100band.json': 'https://huggingface.co/ecker/tortoise-tts-models/resolve/main/models/bigvgan_24khz_100band.json',
|
||||
}
|
||||
|
||||
def hash_file(path, algo="md5", buffer_size=0):
|
||||
import hashlib
|
||||
|
||||
hash = None
|
||||
if algo == "md5":
|
||||
hash = hashlib.md5()
|
||||
elif algo == "sha1":
|
||||
hash = hashlib.sha1()
|
||||
else:
|
||||
raise Exception(f'Unknown hash algorithm specified: {algo}')
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise Exception(f'Path not found: {path}')
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
if buffer_size > 0:
|
||||
while True:
|
||||
data = f.read(buffer_size)
|
||||
if not data:
|
||||
break
|
||||
hash.update(data)
|
||||
else:
|
||||
hash.update(f.read())
|
||||
|
||||
return "{0}".format(hash.hexdigest())
|
||||
|
||||
def check_for_kill_signal():
|
||||
def tqdm_override(arr, verbose=False, progress=None, desc=None):
|
||||
global STOP_SIGNAL
|
||||
if STOP_SIGNAL:
|
||||
STOP_SIGNAL = False
|
||||
raise Exception("Kill signal detected")
|
||||
|
||||
if verbose and desc is not None:
|
||||
print(desc)
|
||||
|
||||
if progress is None:
|
||||
return tqdm(arr, disable=not verbose)
|
||||
return progress.tqdm(arr, desc=desc, track_tqdm=True)
|
||||
|
||||
def download_models(specific_models=None):
|
||||
"""
|
||||
Call to download all the models that Tortoise uses.
|
||||
@ -109,11 +89,6 @@ def download_models(specific_models=None):
|
||||
if os.path.exists(model_path):
|
||||
continue
|
||||
print(f'Downloading {model_name} from {url}...')
|
||||
|
||||
proxy = ProxyHandler({})
|
||||
opener = build_opener(proxy)
|
||||
opener.addheaders = [('User-Agent','mrq/AI-Voice-Cloning')]
|
||||
install_opener(opener)
|
||||
request.urlretrieve(url, model_path, show_progress)
|
||||
print('Done.')
|
||||
|
||||
@ -150,7 +125,7 @@ def load_discrete_vocoder_diffuser(trained_diffusion_steps=4000, desired_diffusi
|
||||
model_var_type='learned_range', loss_type='mse', betas=get_named_beta_schedule('linear', trained_diffusion_steps),
|
||||
conditioning_free=cond_free, conditioning_free_k=cond_free_k)
|
||||
|
||||
@torch.inference_mode()
|
||||
|
||||
def format_conditioning(clip, cond_length=132300, device='cuda', sampling_rate=22050):
|
||||
"""
|
||||
Converts the given conditioning signal to a MEL spectrogram and clips it as expected by the models.
|
||||
@ -162,8 +137,8 @@ def format_conditioning(clip, cond_length=132300, device='cuda', sampling_rate=2
|
||||
rand_start = random.randint(0, gap)
|
||||
clip = clip[:, rand_start:rand_start + cond_length]
|
||||
mel_clip = TorchMelSpectrogram(sampling_rate=sampling_rate)(clip.unsqueeze(0)).squeeze(0)
|
||||
mel_clip = mel_clip.unsqueeze(0)
|
||||
return migrate_to_device(mel_clip, device)
|
||||
return mel_clip.unsqueeze(0).to(device)
|
||||
|
||||
|
||||
def fix_autoregressive_output(codes, stop_token, complain=True):
|
||||
"""
|
||||
@ -194,8 +169,8 @@ def fix_autoregressive_output(codes, stop_token, complain=True):
|
||||
|
||||
return codes
|
||||
|
||||
@torch.inference_mode()
|
||||
def do_spectrogram_diffusion(diffusion_model, diffuser, latents, conditioning_latents, temperature=1, verbose=True, desc=None, sampler="P", input_sample_rate=22050, output_sample_rate=24000):
|
||||
|
||||
def do_spectrogram_diffusion(diffusion_model, diffuser, latents, conditioning_latents, temperature=1, verbose=True, progress=None, desc=None, sampler="P", input_sample_rate=22050, output_sample_rate=24000):
|
||||
"""
|
||||
Uses the specified diffusion model to convert discrete codes into a spectrogram.
|
||||
"""
|
||||
@ -208,7 +183,8 @@ def do_spectrogram_diffusion(diffusion_model, diffuser, latents, conditioning_la
|
||||
|
||||
diffuser.sampler = sampler.lower()
|
||||
mel = diffuser.sample_loop(diffusion_model, output_shape, noise=noise,
|
||||
model_kwargs={'precomputed_aligned_embeddings': precomputed_embeddings}, desc=desc)
|
||||
model_kwargs={'precomputed_aligned_embeddings': precomputed_embeddings},
|
||||
verbose=verbose, progress=progress, desc=desc)
|
||||
|
||||
mel = denormalize_tacotron_mel(mel)[:,:,:output_seq_len]
|
||||
if get_device_name() == "dml":
|
||||
@ -230,37 +206,12 @@ def classify_audio_clip(clip):
|
||||
results = F.softmax(classifier(clip), dim=-1)
|
||||
return results[0][0]
|
||||
|
||||
def migrate_to_device( t, device ):
|
||||
if t is None:
|
||||
return t
|
||||
|
||||
if not hasattr(t, 'device'):
|
||||
t.device = device
|
||||
t.manually_track_device = True
|
||||
elif t.device == device:
|
||||
return t
|
||||
|
||||
if hasattr(t, 'manually_track_device') and t.manually_track_device:
|
||||
t.device = device
|
||||
|
||||
t = t.to(device)
|
||||
|
||||
do_gc()
|
||||
|
||||
return t
|
||||
|
||||
class TextToSpeech:
|
||||
"""
|
||||
Main entry point into Tortoise.
|
||||
"""
|
||||
|
||||
def __init__(self, autoregressive_batch_size=None, models_dir=MODELS_DIR, enable_redaction=True, device=None,
|
||||
minor_optimizations=True,
|
||||
unsqueeze_sample_batches=False,
|
||||
input_sample_rate=22050, output_sample_rate=24000,
|
||||
autoregressive_model_path=None, diffusion_model_path=None, vocoder_model=None, tokenizer_json=None,
|
||||
# ):
|
||||
use_deepspeed=False): # Add use_deepspeed parameter
|
||||
def __init__(self, autoregressive_batch_size=None, models_dir=MODELS_DIR, enable_redaction=True, device=None, minor_optimizations=True, input_sample_rate=22050, output_sample_rate=24000):
|
||||
"""
|
||||
Constructor
|
||||
:param autoregressive_batch_size: Specifies how many samples to generate per batch. Lower this if you are seeing
|
||||
@ -272,48 +223,38 @@ class TextToSpeech:
|
||||
Default is true.
|
||||
:param device: Device to use when running the model. If omitted, the device will be automatically chosen.
|
||||
"""
|
||||
self.loading = True
|
||||
if device is None:
|
||||
device = get_device(verbose=True)
|
||||
|
||||
self.version = [2,4,4] # to-do, autograb this from setup.py, or have setup.py autograb this
|
||||
self.input_sample_rate = input_sample_rate
|
||||
self.output_sample_rate = output_sample_rate
|
||||
self.minor_optimizations = minor_optimizations
|
||||
self.unsqueeze_sample_batches = unsqueeze_sample_batches
|
||||
self.use_deepspeed = use_deepspeed # Store use_deepspeed as an instance variable
|
||||
print(f'use_deepspeed api_debug {use_deepspeed}')
|
||||
# for clarity, it's simpler to split these up and just predicate them on requesting VRAM-consuming optimizations
|
||||
self.preloaded_tensors = minor_optimizations
|
||||
self.use_kv_cache = minor_optimizations
|
||||
if get_device_name() == "dml": # does not work with DirectML
|
||||
print("KV caching requested but not supported with the DirectML backend, disabling...")
|
||||
self.use_kv_cache = False
|
||||
|
||||
self.models_dir = models_dir
|
||||
self.autoregressive_batch_size = get_device_batch_size() if autoregressive_batch_size is None or autoregressive_batch_size == 0 else autoregressive_batch_size
|
||||
self.enable_redaction = enable_redaction
|
||||
self.device = device
|
||||
if self.enable_redaction:
|
||||
self.aligner = Wav2VecAlignment(device='cpu' if get_device_name() == "dml" else self.device)
|
||||
self.aligner = Wav2VecAlignment(device=None)
|
||||
|
||||
self.load_tokenizer_json(tokenizer_json)
|
||||
self.tokenizer = VoiceBpeTokenizer()
|
||||
|
||||
if os.path.exists(f'{models_dir}/autoregressive.ptt'):
|
||||
# Assume this is a traced directory.
|
||||
self.autoregressive = torch.jit.load(f'{models_dir}/autoregressive.ptt')
|
||||
else:
|
||||
if not autoregressive_model_path or not os.path.exists(autoregressive_model_path):
|
||||
autoregressive_model_path = get_model_path('autoregressive.pth', models_dir)
|
||||
|
||||
self.load_autoregressive_model(autoregressive_model_path)
|
||||
|
||||
if os.path.exists(f'{models_dir}/diffusion_decoder.ptt'):
|
||||
self.diffusion = torch.jit.load(f'{models_dir}/diffusion_decoder.ptt')
|
||||
else:
|
||||
if not diffusion_model_path or not os.path.exists(diffusion_model_path):
|
||||
diffusion_model_path = get_model_path('diffusion_decoder.pth', models_dir)
|
||||
self.autoregressive = UnifiedVoice(max_mel_tokens=604, max_text_tokens=402, max_conditioning_inputs=2, layers=30,
|
||||
model_dim=1024,
|
||||
heads=16, number_text_tokens=255, start_text_token=255, checkpointing=False,
|
||||
train_solo_embeddings=False).cpu().eval()
|
||||
self.autoregressive.load_state_dict(torch.load(get_model_path('autoregressive.pth', models_dir)))
|
||||
self.autoregressive.post_init_gpt2_config(kv_cache=minor_optimizations)
|
||||
|
||||
self.load_diffusion_model(diffusion_model_path)
|
||||
self.diffusion = DiffusionTts(model_channels=1024, num_layers=10, in_channels=100, out_channels=200,
|
||||
in_latent_channels=1024, in_tokens=8193, dropout=0, use_fp16=False, num_heads=16,
|
||||
layer_drop=0, unconditioned_percentage=0).cpu().eval()
|
||||
self.diffusion.load_state_dict(torch.load(get_model_path('diffusion_decoder.pth', models_dir)))
|
||||
|
||||
|
||||
self.clvp = CLVP(dim_text=768, dim_speech=768, dim_latent=768, num_text_tokens=256, text_enc_depth=20,
|
||||
@ -323,168 +264,19 @@ class TextToSpeech:
|
||||
self.clvp.load_state_dict(torch.load(get_model_path('clvp2.pth', models_dir)))
|
||||
self.cvvp = None # CVVP model is only loaded if used.
|
||||
|
||||
self.vocoder_model = vocoder_model
|
||||
self.load_vocoder_model(self.vocoder_model)
|
||||
self.vocoder = UnivNetGenerator().cpu()
|
||||
self.vocoder.load_state_dict(torch.load(get_model_path('vocoder.pth', models_dir), map_location=torch.device('cpu'))['model_g'])
|
||||
self.vocoder.eval(inference=True)
|
||||
|
||||
# Random latent generators (RLGs) are loaded lazily.
|
||||
self.rlg_auto = None
|
||||
self.rlg_diffusion = None
|
||||
|
||||
if self.preloaded_tensors:
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, self.device )
|
||||
self.diffusion = migrate_to_device( self.diffusion, self.device )
|
||||
self.clvp = migrate_to_device( self.clvp, self.device )
|
||||
self.vocoder = migrate_to_device( self.vocoder, self.device )
|
||||
|
||||
self.loading = False
|
||||
|
||||
def load_autoregressive_model(self, autoregressive_model_path, is_xtts=False):
|
||||
if hasattr(self,"autoregressive_model_path") and os.path.samefile(self.autoregressive_model_path, autoregressive_model_path):
|
||||
return
|
||||
|
||||
self.autoregressive_model_path = autoregressive_model_path if autoregressive_model_path and os.path.exists(autoregressive_model_path) else get_model_path('autoregressive.pth', self.models_dir)
|
||||
new_hash = hash_file(self.autoregressive_model_path)
|
||||
|
||||
if hasattr(self,"autoregressive_model_hash") and self.autoregressive_model_hash == new_hash:
|
||||
return
|
||||
|
||||
self.autoregressive_model_hash = new_hash
|
||||
|
||||
self.loading = True
|
||||
print(f"Loading autoregressive model: {self.autoregressive_model_path}")
|
||||
|
||||
if hasattr(self, 'autoregressive'):
|
||||
del self.autoregressive
|
||||
|
||||
# XTTS requires a different "dimensionality" for its autoregressive model
|
||||
if new_hash == "e4ce21eae0043f7691d6a6c8540b74b8" or is_xtts:
|
||||
dimensionality = {
|
||||
"max_mel_tokens": 605,
|
||||
"max_text_tokens": 402,
|
||||
"max_prompt_tokens": 70,
|
||||
"max_conditioning_inputs": 1,
|
||||
"layers": 30,
|
||||
"model_dim": 1024,
|
||||
"heads": 16,
|
||||
"number_text_tokens": 5023, # -1
|
||||
"start_text_token": 261,
|
||||
"stop_text_token": 0,
|
||||
"number_mel_codes": 8194,
|
||||
"start_mel_token": 8192,
|
||||
"stop_mel_token": 8193,
|
||||
}
|
||||
else:
|
||||
dimensionality = {
|
||||
"max_mel_tokens": 604,
|
||||
"max_text_tokens": 402,
|
||||
"max_conditioning_inputs": 2,
|
||||
"layers": 30,
|
||||
"model_dim": 1024,
|
||||
"heads": 16,
|
||||
"number_text_tokens": 255,
|
||||
"start_text_token": 255,
|
||||
"checkpointing": False,
|
||||
"train_solo_embeddings": False
|
||||
}
|
||||
|
||||
self.autoregressive = UnifiedVoice(**dimensionality).cpu().eval()
|
||||
self.autoregressive.load_state_dict(torch.load(self.autoregressive_model_path))
|
||||
self.autoregressive.post_init_gpt2_config(use_deepspeed=self.use_deepspeed, kv_cache=self.use_kv_cache)
|
||||
if self.preloaded_tensors:
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, self.device )
|
||||
|
||||
self.loading = False
|
||||
print(f"Loaded autoregressive model")
|
||||
|
||||
def load_diffusion_model(self, diffusion_model_path):
|
||||
if hasattr(self,"diffusion_model_path") and os.path.samefile(self.diffusion_model_path, diffusion_model_path):
|
||||
return
|
||||
|
||||
self.loading = True
|
||||
|
||||
self.diffusion_model_path = diffusion_model_path if diffusion_model_path and os.path.exists(diffusion_model_path) else get_model_path('diffusion_decoder.pth', self.models_dir)
|
||||
self.diffusion_model_hash = hash_file(self.diffusion_model_path)
|
||||
|
||||
if hasattr(self, 'diffusion'):
|
||||
del self.diffusion
|
||||
|
||||
# XTTS does not require a different "dimensionality" for its diffusion model
|
||||
dimensionality = {
|
||||
"model_channels": 1024,
|
||||
"num_layers": 10,
|
||||
"in_channels": 100,
|
||||
"out_channels": 200,
|
||||
"in_latent_channels": 1024,
|
||||
"in_tokens": 8193,
|
||||
"dropout": 0,
|
||||
"use_fp16": False,
|
||||
"num_heads": 16,
|
||||
"layer_drop": 0,
|
||||
"unconditioned_percentage": 0
|
||||
}
|
||||
self.diffusion = DiffusionTts(**dimensionality)
|
||||
self.diffusion.load_state_dict(torch.load(get_model_path('diffusion_decoder.pth', self.models_dir)))
|
||||
if self.preloaded_tensors:
|
||||
self.diffusion = migrate_to_device( self.diffusion, self.device )
|
||||
|
||||
self.loading = False
|
||||
print(f"Loaded diffusion model")
|
||||
|
||||
def load_vocoder_model(self, vocoder_model):
|
||||
if hasattr(self,"vocoder_model_path") and os.path.samefile(self.vocoder_model_path, vocoder_model):
|
||||
return
|
||||
|
||||
self.loading = True
|
||||
|
||||
if hasattr(self, 'vocoder'):
|
||||
del self.vocoder
|
||||
|
||||
print("Loading vocoder model:", vocoder_model)
|
||||
if vocoder_model is None:
|
||||
vocoder_model = 'bigvgan_24khz_100band'
|
||||
|
||||
if 'bigvgan' in vocoder_model:
|
||||
# credit to https://github.com/deviandice / https://git.ecker.tech/mrq/ai-voice-cloning/issues/52
|
||||
vocoder_key = 'generator'
|
||||
self.vocoder_model_path = 'bigvgan_24khz_100band.pth'
|
||||
if f'{vocoder_model}.pth' in MODELS:
|
||||
self.vocoder_model_path = f'{vocoder_model}.pth'
|
||||
vocoder_config = 'bigvgan_24khz_100band.json'
|
||||
if f'{vocoder_model}.json' in MODELS:
|
||||
vocoder_config = f'{vocoder_model}.json'
|
||||
vocoder_config = get_model_path(vocoder_config, self.models_dir)
|
||||
|
||||
self.vocoder = BigVGAN(config=vocoder_config).cpu()
|
||||
#elif vocoder_model == "univnet":
|
||||
else:
|
||||
vocoder_key = 'model_g'
|
||||
self.vocoder_model_path = 'vocoder.pth'
|
||||
self.vocoder = UnivNetGenerator().cpu()
|
||||
|
||||
print(f"Loading vocoder model: {self.vocoder_model_path}")
|
||||
self.vocoder.load_state_dict(torch.load(get_model_path(self.vocoder_model_path, self.models_dir), map_location=torch.device('cpu'))[vocoder_key])
|
||||
|
||||
self.vocoder.eval(inference=True)
|
||||
if self.preloaded_tensors:
|
||||
self.vocoder = migrate_to_device( self.vocoder, self.device )
|
||||
self.loading = False
|
||||
print(f"Loaded vocoder model")
|
||||
|
||||
def load_tokenizer_json(self, tokenizer_json):
|
||||
if hasattr(self,"tokenizer_json") and os.path.samefile(self.tokenizer_json, tokenizer_json):
|
||||
return
|
||||
|
||||
self.loading = True
|
||||
self.tokenizer_json = tokenizer_json if tokenizer_json else os.path.join(os.path.dirname(os.path.realpath(__file__)), '../tortoise/data/tokenizer.json')
|
||||
print("Loading tokenizer JSON:", self.tokenizer_json)
|
||||
|
||||
if hasattr(self, 'tokenizer'):
|
||||
del self.tokenizer
|
||||
|
||||
self.tokenizer = VoiceBpeTokenizer(vocab_file=self.tokenizer_json)
|
||||
|
||||
self.loading = False
|
||||
print(f"Loaded tokenizer")
|
||||
if self.minor_optimizations:
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
self.diffusion = self.diffusion.to(self.device)
|
||||
self.clvp = self.clvp.to(self.device)
|
||||
self.vocoder = self.vocoder.to(self.device)
|
||||
|
||||
def load_cvvp(self):
|
||||
"""Load CVVP model."""
|
||||
@ -492,96 +284,86 @@ class TextToSpeech:
|
||||
speech_enc_depth=8, speech_mask_percentage=0, latent_multiplier=1).cpu().eval()
|
||||
self.cvvp.load_state_dict(torch.load(get_model_path('cvvp.pth', self.models_dir)))
|
||||
|
||||
if self.preloaded_tensors:
|
||||
self.cvvp = migrate_to_device( self.cvvp, self.device )
|
||||
if self.minor_optimizations:
|
||||
self.cvvp = self.cvvp.to(self.device)
|
||||
|
||||
@torch.inference_mode()
|
||||
def get_conditioning_latents(self, voice_samples, return_mels=False, verbose=False, slices=1, max_chunk_size=None, force_cpu=False, original_ar=False, original_diffusion=False):
|
||||
def get_conditioning_latents(self, voice_samples, return_mels=False, verbose=False, progress=None, chunk_size=None, max_chunk_size=None, chunk_tensors=True):
|
||||
"""
|
||||
Transforms one or more voice_samples into a tuple (autoregressive_conditioning_latent, diffusion_conditioning_latent).
|
||||
These are expressive learned latents that encode aspects of the provided clips like voice, intonation, and acoustic
|
||||
properties.
|
||||
:param voice_samples: List of 2 or more ~10 second reference clips, which should be torch tensors containing 22.05kHz waveform data.
|
||||
"""
|
||||
|
||||
with torch.no_grad():
|
||||
# computing conditional latents requires being done on the CPU if using DML because M$ still hasn't implemented some core functions
|
||||
if get_device_name() == "dml":
|
||||
force_cpu = True
|
||||
device = torch.device('cpu') if force_cpu else self.device
|
||||
device = 'cpu' if get_device_name() == "dml" else self.device
|
||||
|
||||
if not isinstance(voice_samples, list):
|
||||
voice_samples = [voice_samples]
|
||||
|
||||
resampler_22K = torchaudio.transforms.Resample(
|
||||
self.input_sample_rate,
|
||||
22050,
|
||||
lowpass_filter_width=16,
|
||||
rolloff=0.85,
|
||||
resampling_method="kaiser_window",
|
||||
beta=8.555504641634386,
|
||||
).to(device)
|
||||
|
||||
resampler_24K = torchaudio.transforms.Resample(
|
||||
self.input_sample_rate,
|
||||
24000,
|
||||
lowpass_filter_width=16,
|
||||
rolloff=0.85,
|
||||
resampling_method="kaiser_window",
|
||||
beta=8.555504641634386,
|
||||
).to(device)
|
||||
|
||||
voice_samples = [migrate_to_device(v, device) for v in voice_samples]
|
||||
voice_samples = [v.to(device) for v in voice_samples]
|
||||
|
||||
auto_conds = []
|
||||
diffusion_conds = []
|
||||
|
||||
if original_ar:
|
||||
samples = [resampler_22K(sample) for sample in voice_samples]
|
||||
for sample in tqdm(samples, desc="Computing AR conditioning latents..."):
|
||||
auto_conds.append(format_conditioning(sample, device=device, sampling_rate=self.input_sample_rate, cond_length=132300))
|
||||
else:
|
||||
samples = [resampler_22K(sample) for sample in voice_samples]
|
||||
concat = torch.cat(samples, dim=-1)
|
||||
chunk_size = concat.shape[-1]
|
||||
|
||||
if slices == 0:
|
||||
slices = 1
|
||||
elif max_chunk_size is not None and chunk_size > max_chunk_size:
|
||||
slices = 1
|
||||
while int(chunk_size / slices) > max_chunk_size:
|
||||
slices = slices + 1
|
||||
|
||||
chunks = torch.chunk(concat, slices, dim=1)
|
||||
chunk_size = chunks[0].shape[-1]
|
||||
|
||||
for chunk in tqdm(chunks, desc="Computing AR conditioning latents..."):
|
||||
auto_conds.append(format_conditioning(chunk, device=device, sampling_rate=self.input_sample_rate, cond_length=chunk_size))
|
||||
|
||||
|
||||
if original_diffusion:
|
||||
samples = [resampler_24K(sample) for sample in voice_samples]
|
||||
for sample in tqdm(samples, desc="Computing diffusion conditioning latents..."):
|
||||
sample = pad_or_truncate(sample, 102400)
|
||||
cond_mel = wav_to_univnet_mel(migrate_to_device(sample, device), do_normalization=False, device=self.device)
|
||||
diffusion_conds.append(cond_mel)
|
||||
else:
|
||||
samples = [resampler_24K(sample) for sample in voice_samples]
|
||||
for chunk in tqdm(chunks, desc="Computing diffusion conditioning latents..."):
|
||||
check_for_kill_signal()
|
||||
chunk = pad_or_truncate(chunk, chunk_size)
|
||||
cond_mel = wav_to_univnet_mel(migrate_to_device( chunk, device ), do_normalization=False, device=device)
|
||||
diffusion_conds.append(cond_mel)
|
||||
if not isinstance(voice_samples, list):
|
||||
voice_samples = [voice_samples]
|
||||
for vs in voice_samples:
|
||||
auto_conds.append(format_conditioning(vs, device=device, sampling_rate=self.input_sample_rate))
|
||||
|
||||
auto_conds = torch.stack(auto_conds, dim=1)
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, device )
|
||||
auto_latent = self.autoregressive.get_conditioning(auto_conds)
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, self.device if self.preloaded_tensors else 'cpu' )
|
||||
|
||||
diffusion_conds = []
|
||||
|
||||
samples = [] # resample in its own pass to make things easier
|
||||
for sample in voice_samples:
|
||||
# The diffuser operates at a sample rate of 24000 (except for the latent inputs)
|
||||
#samples.append(torchaudio.functional.resample(sample, 22050, 24000))
|
||||
samples.append(torchaudio.functional.resample(sample, self.input_sample_rate, self.output_sample_rate))
|
||||
|
||||
if chunk_size is None:
|
||||
for sample in tqdm_override(samples, verbose=verbose and len(samples) > 1, progress=progress if len(samples) > 1 else None, desc="Calculating size of best fit..."):
|
||||
if chunk_tensors:
|
||||
chunk_size = sample.shape[-1] if chunk_size is None else min( chunk_size, sample.shape[-1] )
|
||||
else:
|
||||
chunk_size = sample.shape[-1] if chunk_size is None else max( chunk_size, sample.shape[-1] )
|
||||
|
||||
print(f"Size of best fit: {chunk_size}")
|
||||
if max_chunk_size is not None and chunk_size > max_chunk_size:
|
||||
chunk_size = max_chunk_size
|
||||
print(f"Chunk size exceeded, clamping to: {max_chunk_size}")
|
||||
|
||||
chunks = []
|
||||
if chunk_tensors:
|
||||
for sample in tqdm_override(samples, verbose=verbose, progress=progress, desc="Slicing samples into chunks..."):
|
||||
sliced = torch.chunk(sample, int(sample.shape[-1] / chunk_size) + 1, dim=1)
|
||||
for s in sliced:
|
||||
chunks.append(s)
|
||||
else:
|
||||
chunks = samples
|
||||
|
||||
for chunk in tqdm_override(chunks, verbose=verbose, progress=progress, desc="Computing conditioning latents..."):
|
||||
chunk = pad_or_truncate(chunk, chunk_size)
|
||||
cond_mel = wav_to_univnet_mel(chunk.to(device), do_normalization=False, device=device)
|
||||
diffusion_conds.append(cond_mel)
|
||||
|
||||
diffusion_conds = torch.stack(diffusion_conds, dim=1)
|
||||
self.diffusion = migrate_to_device( self.diffusion, device )
|
||||
diffusion_latent = self.diffusion.get_conditioning(diffusion_conds)
|
||||
self.diffusion = migrate_to_device( self.diffusion, self.device if self.preloaded_tensors else 'cpu' )
|
||||
|
||||
# required since DML implementation screams about falling back to CPU, but crashes anyways
|
||||
if self.minor_optimizations:
|
||||
if get_device_name() == "dml":
|
||||
self.autoregressive = self.autoregressive.cpu()
|
||||
auto_latent = self.autoregressive.get_conditioning(auto_conds)
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
|
||||
self.diffusion = self.diffusion.cpu()
|
||||
diffusion_latent = self.diffusion.get_conditioning(diffusion_conds)
|
||||
self.diffusion = self.diffusion.to(self.device)
|
||||
else:
|
||||
auto_latent = self.autoregressive.get_conditioning(auto_conds)
|
||||
diffusion_latent = self.diffusion.get_conditioning(diffusion_conds)
|
||||
else:
|
||||
self.autoregressive = self.autoregressive.to(device)
|
||||
auto_latent = self.autoregressive.get_conditioning(auto_conds)
|
||||
self.autoregressive = self.autoregressive.cpu()
|
||||
|
||||
self.diffusion = self.diffusion.to(device)
|
||||
diffusion_latent = self.diffusion.get_conditioning(diffusion_conds)
|
||||
self.diffusion = self.diffusion.cpu()
|
||||
|
||||
if return_mels:
|
||||
return auto_latent, diffusion_latent, auto_conds, diffusion_conds
|
||||
@ -621,15 +403,11 @@ class TextToSpeech:
|
||||
settings.update(kwargs) # allow overriding of preset settings with kwargs
|
||||
return self.tts(text, **settings)
|
||||
|
||||
@torch.inference_mode()
|
||||
def tts(self, text, voice_samples=None, conditioning_latents=None, k=1, verbose=True, use_deterministic_seed=None,
|
||||
return_deterministic_state=False,
|
||||
# autoregressive generation parameters follow
|
||||
num_autoregressive_samples=512, temperature=.8, length_penalty=1, repetition_penalty=2.0, top_p=.8, max_mel_tokens=500,
|
||||
sample_batch_size=None,
|
||||
autoregressive_model=None,
|
||||
diffusion_model=None,
|
||||
tokenizer_json=None,
|
||||
# CVVP parameters follow
|
||||
cvvp_amount=.0,
|
||||
# diffusion generation parameters follow
|
||||
@ -637,6 +415,7 @@ class TextToSpeech:
|
||||
diffusion_sampler="P",
|
||||
breathing_room=8,
|
||||
half_p=False,
|
||||
progress=None,
|
||||
**hf_generate_kwargs):
|
||||
"""
|
||||
Produces an audio clip of the given text being spoken with the given reference voice.
|
||||
@ -683,32 +462,13 @@ class TextToSpeech:
|
||||
:return: Generated audio clip(s) as a torch tensor. Shape 1,S if k=1 else, (k,1,S) where S is the sample length.
|
||||
Sample rate is 24kHz.
|
||||
"""
|
||||
|
||||
if get_device_name() == "dml" and half_p:
|
||||
print("Float16 requested but not supported with the DirectML backend, disabling...")
|
||||
if get_device_name() == "dml":
|
||||
half_p = False
|
||||
|
||||
self.diffusion.enable_fp16 = half_p
|
||||
deterministic_seed = self.deterministic_state(seed=use_deterministic_seed)
|
||||
|
||||
if autoregressive_model is None:
|
||||
autoregressive_model = self.autoregressive_model_path
|
||||
elif autoregressive_model != self.autoregressive_model_path:
|
||||
self.load_autoregressive_model(autoregressive_model)
|
||||
|
||||
if diffusion_model is None:
|
||||
diffusion_model = self.diffusion_model_path
|
||||
elif diffusion_model != self.diffusion_model_path:
|
||||
self.load_diffusion_model(diffusion_model)
|
||||
|
||||
if tokenizer_json is None:
|
||||
tokenizer_json = self.tokenizer_json
|
||||
elif tokenizer_json != self.tokenizer_json:
|
||||
self.load_tokenizer_json(tokenizer_json)
|
||||
|
||||
text_tokens = torch.IntTensor(self.tokenizer.encode(text)).unsqueeze(0)
|
||||
text_tokens = migrate_to_device( text_tokens, self.device )
|
||||
|
||||
text_tokens = torch.IntTensor(self.tokenizer.encode(text)).unsqueeze(0).to(self.device)
|
||||
text_tokens = F.pad(text_tokens, (0, 1)) # This may not be necessary.
|
||||
assert text_tokens.shape[-1] < 400, 'Too much text provided. Break the text up into separate segments and re-try inference.'
|
||||
|
||||
@ -723,6 +483,8 @@ class TextToSpeech:
|
||||
auto_conditioning, diffusion_conditioning, auto_conds, _ = conditioning_latents
|
||||
else:
|
||||
auto_conditioning, diffusion_conditioning = self.get_random_conditioning_latents()
|
||||
auto_conditioning = auto_conditioning.to(self.device)
|
||||
diffusion_conditioning = diffusion_conditioning.to(self.device)
|
||||
|
||||
diffuser = load_discrete_vocoder_diffuser(desired_diffusion_steps=diffusion_iterations, cond_free=cond_free, cond_free_k=cond_free_k)
|
||||
|
||||
@ -735,14 +497,12 @@ class TextToSpeech:
|
||||
num_autoregressive_samples = 1
|
||||
stop_mel_token = self.autoregressive.stop_mel_token
|
||||
calm_token = 83 # This is the token for coding silence, which is fixed in place with "fix_autoregressive_output"
|
||||
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, self.device )
|
||||
auto_conditioning = migrate_to_device( auto_conditioning, self.device )
|
||||
text_tokens = migrate_to_device( text_tokens, self.device )
|
||||
|
||||
|
||||
if not self.minor_optimizations:
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
|
||||
with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=half_p):
|
||||
for b in tqdm(range(num_batches), desc="Generating autoregressive samples"):
|
||||
check_for_kill_signal()
|
||||
for b in tqdm_override(range(num_batches), verbose=verbose, progress=progress, desc="Generating autoregressive samples"):
|
||||
codes = self.autoregressive.inference_speech(auto_conditioning, text_tokens,
|
||||
do_sample=True,
|
||||
top_p=top_p,
|
||||
@ -756,31 +516,18 @@ class TextToSpeech:
|
||||
codes = F.pad(codes, (0, padding_needed), value=stop_mel_token)
|
||||
samples.append(codes)
|
||||
|
||||
if not self.preloaded_tensors:
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, 'cpu' )
|
||||
|
||||
if self.unsqueeze_sample_batches:
|
||||
new_samples = []
|
||||
for batch in samples:
|
||||
for i in range(batch.shape[0]):
|
||||
new_samples.append(batch[i].unsqueeze(0))
|
||||
samples = new_samples
|
||||
|
||||
clip_results = []
|
||||
if auto_conds is not None:
|
||||
auto_conditioning = migrate_to_device( auto_conditioning, self.device )
|
||||
|
||||
with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=half_p):
|
||||
if not self.preloaded_tensors:
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, 'cpu' )
|
||||
self.clvp = migrate_to_device( self.clvp, self.device )
|
||||
if not self.minor_optimizations:
|
||||
self.autoregressive = self.autoregressive.cpu()
|
||||
self.clvp = self.clvp.to(self.device)
|
||||
|
||||
if cvvp_amount > 0:
|
||||
if self.cvvp is None:
|
||||
self.load_cvvp()
|
||||
|
||||
if not self.preloaded_tensors:
|
||||
self.cvvp = migrate_to_device( self.cvvp, self.device )
|
||||
if not self.minor_optimizations:
|
||||
self.cvvp = self.cvvp.to(self.device)
|
||||
|
||||
desc="Computing best candidates"
|
||||
if verbose:
|
||||
@ -789,15 +536,11 @@ class TextToSpeech:
|
||||
else:
|
||||
desc = f"Computing best candidates using CLVP {((1-cvvp_amount) * 100):2.0f}% and CVVP {(cvvp_amount * 100):2.0f}%"
|
||||
|
||||
|
||||
for batch in tqdm(samples, desc=desc):
|
||||
check_for_kill_signal()
|
||||
for batch in tqdm_override(samples, verbose=verbose, progress=progress, desc=desc):
|
||||
for i in range(batch.shape[0]):
|
||||
batch[i] = fix_autoregressive_output(batch[i], stop_mel_token)
|
||||
|
||||
if cvvp_amount != 1:
|
||||
clvp = self.clvp(text_tokens.repeat(batch.shape[0], 1), batch, return_loss=False)
|
||||
|
||||
if auto_conds is not None and cvvp_amount > 0:
|
||||
cvvp_accumulator = 0
|
||||
for cl in range(auto_conds.shape[1]):
|
||||
@ -810,53 +553,47 @@ class TextToSpeech:
|
||||
else:
|
||||
clip_results.append(clvp)
|
||||
|
||||
if not self.preloaded_tensors and auto_conds is not None:
|
||||
auto_conds = migrate_to_device( auto_conds, 'cpu' )
|
||||
|
||||
clip_results = torch.cat(clip_results, dim=0)
|
||||
samples = torch.cat(samples, dim=0)
|
||||
if k < num_autoregressive_samples:
|
||||
best_results = samples[torch.topk(clip_results, k=k).indices]
|
||||
else:
|
||||
best_results = samples
|
||||
|
||||
if not self.preloaded_tensors:
|
||||
self.clvp = migrate_to_device( self.clvp, 'cpu' )
|
||||
self.cvvp = migrate_to_device( self.cvvp, 'cpu' )
|
||||
best_results = samples[torch.topk(clip_results, k=k).indices]
|
||||
|
||||
|
||||
if get_device_name() == "dml":
|
||||
text_tokens = migrate_to_device( text_tokens, 'cpu' )
|
||||
best_results = migrate_to_device( best_results, 'cpu' )
|
||||
auto_conditioning = migrate_to_device( auto_conditioning, 'cpu' )
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, 'cpu' )
|
||||
else:
|
||||
auto_conditioning = auto_conditioning.to(self.device)
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
if not self.minor_optimizations:
|
||||
self.clvp = self.clvp.cpu()
|
||||
if self.cvvp is not None:
|
||||
self.cvvp = self.cvvp.cpu()
|
||||
|
||||
del samples
|
||||
|
||||
# The diffusion model actually wants the last hidden layer from the autoregressive model as conditioning
|
||||
# inputs. Re-produce those for the top results. This could be made more efficient by storing all of these
|
||||
# results, but will increase memory usage.
|
||||
if not self.minor_optimizations:
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
|
||||
if get_device_name() == "dml":
|
||||
text_tokens = text_tokens.cpu()
|
||||
best_results = best_results.cpu()
|
||||
auto_conditioning = auto_conditioning.cpu()
|
||||
self.autoregressive = self.autoregressive.cpu()
|
||||
|
||||
best_latents = self.autoregressive(auto_conditioning.repeat(k, 1), text_tokens.repeat(k, 1),
|
||||
torch.tensor([text_tokens.shape[-1]], device=text_tokens.device), best_results,
|
||||
torch.tensor([best_results.shape[-1]*self.autoregressive.mel_length_compression], device=text_tokens.device),
|
||||
return_latent=True, clip_inputs=False)
|
||||
|
||||
diffusion_conditioning = migrate_to_device( diffusion_conditioning, self.device )
|
||||
|
||||
if get_device_name() == "dml":
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, self.device )
|
||||
best_results = migrate_to_device( best_results, self.device )
|
||||
best_latents = migrate_to_device( best_latents, self.device )
|
||||
self.vocoder = migrate_to_device( self.vocoder, 'cpu' )
|
||||
else:
|
||||
if not self.preloaded_tensors:
|
||||
self.autoregressive = migrate_to_device( self.autoregressive, 'cpu' )
|
||||
|
||||
self.diffusion = migrate_to_device( self.diffusion, self.device )
|
||||
self.vocoder = migrate_to_device( self.vocoder, self.device )
|
||||
self.autoregressive = self.autoregressive.to(self.device)
|
||||
best_results = best_results.to(self.device)
|
||||
best_latents = best_latents.to(self.device)
|
||||
|
||||
if not self.minor_optimizations:
|
||||
self.autoregressive = self.autoregressive.cpu()
|
||||
self.diffusion = self.diffusion.to(self.device)
|
||||
self.vocoder = self.vocoder.to(self.device)
|
||||
|
||||
if get_device_name() == "dml":
|
||||
self.vocoder = self.vocoder.cpu()
|
||||
|
||||
del text_tokens
|
||||
del auto_conditioning
|
||||
@ -878,21 +615,19 @@ class TextToSpeech:
|
||||
break
|
||||
|
||||
mel = do_spectrogram_diffusion(self.diffusion, diffuser, latents, diffusion_conditioning,
|
||||
temperature=diffusion_temperature, desc="Transforming autoregressive outputs into audio..", sampler=diffusion_sampler,
|
||||
temperature=diffusion_temperature, verbose=verbose, progress=progress, desc="Transforming autoregressive outputs into audio..", sampler=diffusion_sampler,
|
||||
input_sample_rate=self.input_sample_rate, output_sample_rate=self.output_sample_rate)
|
||||
|
||||
wav = self.vocoder.inference(mel)
|
||||
wav_candidates.append(wav)
|
||||
wav_candidates.append(wav.cpu())
|
||||
|
||||
if not self.preloaded_tensors:
|
||||
self.diffusion = migrate_to_device( self.diffusion, 'cpu' )
|
||||
self.vocoder = migrate_to_device( self.vocoder, 'cpu' )
|
||||
if not self.minor_optimizations:
|
||||
self.diffusion = self.diffusion.cpu()
|
||||
self.vocoder = self.vocoder.cpu()
|
||||
|
||||
def potentially_redact(clip, text):
|
||||
if self.enable_redaction:
|
||||
t = clip.squeeze(1)
|
||||
t = migrate_to_device( t, 'cpu' if get_device_name() == "dml" else self.device)
|
||||
return self.aligner.redact(t, text, self.output_sample_rate).unsqueeze(1)
|
||||
return self.aligner.redact(clip.squeeze(1), text, self.output_sample_rate).unsqueeze(1)
|
||||
return clip
|
||||
wav_candidates = [potentially_redact(wav_candidate, text) for wav_candidate in wav_candidates]
|
||||
|
||||
@ -901,7 +636,7 @@ class TextToSpeech:
|
||||
else:
|
||||
res = wav_candidates[0]
|
||||
|
||||
do_gc()
|
||||
gc.collect()
|
||||
|
||||
if return_deterministic_state:
|
||||
return res, (deterministic_seed, text, voice_samples, conditioning_latents)
|
||||
|
||||
@ -14,7 +14,6 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--voice', type=str, help='Selects the voice to use for generation. See options in voices/ directory (and add your own!) '
|
||||
'Use the & character to join two voices together. Use a comma to perform inference on multiple voices.', default='random')
|
||||
parser.add_argument('--preset', type=str, help='Which voice preset to use.', default='standard')
|
||||
parser.add_argument('--use_deepspeed', type=bool, help='Use deepspeed for speed bump.', default=True)
|
||||
parser.add_argument('--output_path', type=str, help='Where to store outputs.', default='results/')
|
||||
parser.add_argument('--model_dir', type=str, help='Where to find pretrained model checkpoints. Tortoise automatically downloads these to .models, so this'
|
||||
'should only be specified if you have custom checkpoints.', default=MODELS_DIR)
|
||||
@ -38,8 +37,8 @@ if __name__ == '__main__':
|
||||
|
||||
|
||||
os.makedirs(args.output_path, exist_ok=True)
|
||||
#print(f'use_deepspeed do_tts_debug {use_deepspeed}')
|
||||
tts = TextToSpeech(models_dir=args.model_dir, use_deepspeed=args.use_deepspeed)
|
||||
|
||||
tts = TextToSpeech(models_dir=args.model_dir)
|
||||
|
||||
selected_voices = args.voice.split(',')
|
||||
for k, selected_voice in enumerate(selected_voices):
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
# Implementation adapted from https://github.com/EdwardDixon/snake under the MIT license.
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import torch
|
||||
from torch import nn, sin, pow
|
||||
from torch.nn import Parameter
|
||||
|
||||
|
||||
class Snake(nn.Module):
|
||||
'''
|
||||
Implementation of a sine-based periodic activation function
|
||||
Shape:
|
||||
- Input: (B, C, T)
|
||||
- Output: (B, C, T), same shape as the input
|
||||
Parameters:
|
||||
- alpha - trainable parameter
|
||||
References:
|
||||
- This activation function is from this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda:
|
||||
https://arxiv.org/abs/2006.08195
|
||||
Examples:
|
||||
>>> a1 = snake(256)
|
||||
>>> x = torch.randn(256)
|
||||
>>> x = a1(x)
|
||||
'''
|
||||
def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False):
|
||||
'''
|
||||
Initialization.
|
||||
INPUT:
|
||||
- in_features: shape of the input
|
||||
- alpha: trainable parameter
|
||||
alpha is initialized to 1 by default, higher values = higher-frequency.
|
||||
alpha will be trained along with the rest of your model.
|
||||
'''
|
||||
super(Snake, self).__init__()
|
||||
self.in_features = in_features
|
||||
|
||||
# initialize alpha
|
||||
self.alpha_logscale = alpha_logscale
|
||||
if self.alpha_logscale: # log scale alphas initialized to zeros
|
||||
self.alpha = Parameter(torch.zeros(in_features) * alpha)
|
||||
else: # linear scale alphas initialized to ones
|
||||
self.alpha = Parameter(torch.ones(in_features) * alpha)
|
||||
|
||||
self.alpha.requires_grad = alpha_trainable
|
||||
|
||||
self.no_div_by_zero = 0.000000001
|
||||
|
||||
def forward(self, x):
|
||||
'''
|
||||
Forward pass of the function.
|
||||
Applies the function to the input elementwise.
|
||||
Snake ∶= x + 1/a * sin^2 (xa)
|
||||
'''
|
||||
alpha = self.alpha.unsqueeze(0).unsqueeze(-1) # line up with x to [B, C, T]
|
||||
if self.alpha_logscale:
|
||||
alpha = torch.exp(alpha)
|
||||
x = x + (1.0 / (alpha + self.no_div_by_zero)) * pow(sin(x * alpha), 2)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class SnakeBeta(nn.Module):
|
||||
'''
|
||||
A modified Snake function which uses separate parameters for the magnitude of the periodic components
|
||||
Shape:
|
||||
- Input: (B, C, T)
|
||||
- Output: (B, C, T), same shape as the input
|
||||
Parameters:
|
||||
- alpha - trainable parameter that controls frequency
|
||||
- beta - trainable parameter that controls magnitude
|
||||
References:
|
||||
- This activation function is a modified version based on this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda:
|
||||
https://arxiv.org/abs/2006.08195
|
||||
Examples:
|
||||
>>> a1 = snakebeta(256)
|
||||
>>> x = torch.randn(256)
|
||||
>>> x = a1(x)
|
||||
'''
|
||||
def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False):
|
||||
'''
|
||||
Initialization.
|
||||
INPUT:
|
||||
- in_features: shape of the input
|
||||
- alpha - trainable parameter that controls frequency
|
||||
- beta - trainable parameter that controls magnitude
|
||||
alpha is initialized to 1 by default, higher values = higher-frequency.
|
||||
beta is initialized to 1 by default, higher values = higher-magnitude.
|
||||
alpha will be trained along with the rest of your model.
|
||||
'''
|
||||
super(SnakeBeta, self).__init__()
|
||||
self.in_features = in_features
|
||||
|
||||
# initialize alpha
|
||||
self.alpha_logscale = alpha_logscale
|
||||
if self.alpha_logscale: # log scale alphas initialized to zeros
|
||||
self.alpha = Parameter(torch.zeros(in_features) * alpha)
|
||||
self.beta = Parameter(torch.zeros(in_features) * alpha)
|
||||
else: # linear scale alphas initialized to ones
|
||||
self.alpha = Parameter(torch.ones(in_features) * alpha)
|
||||
self.beta = Parameter(torch.ones(in_features) * alpha)
|
||||
|
||||
self.alpha.requires_grad = alpha_trainable
|
||||
self.beta.requires_grad = alpha_trainable
|
||||
|
||||
self.no_div_by_zero = 0.000000001
|
||||
|
||||
def forward(self, x):
|
||||
'''
|
||||
Forward pass of the function.
|
||||
Applies the function to the input elementwise.
|
||||
SnakeBeta ∶= x + 1/b * sin^2 (xa)
|
||||
'''
|
||||
alpha = self.alpha.unsqueeze(0).unsqueeze(-1) # line up with x to [B, C, T]
|
||||
beta = self.beta.unsqueeze(0).unsqueeze(-1)
|
||||
if self.alpha_logscale:
|
||||
alpha = torch.exp(alpha)
|
||||
beta = torch.exp(beta)
|
||||
x = x + (1.0 / (beta + self.no_div_by_zero)) * pow(sin(x * alpha), 2)
|
||||
|
||||
return x
|
||||
@ -1,6 +0,0 @@
|
||||
# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
from .filter import *
|
||||
from .resample import *
|
||||
from .act import *
|
||||
@ -1,28 +0,0 @@
|
||||
# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import torch.nn as nn
|
||||
from .resample import UpSample1d, DownSample1d
|
||||
|
||||
|
||||
class Activation1d(nn.Module):
|
||||
def __init__(self,
|
||||
activation,
|
||||
up_ratio: int = 2,
|
||||
down_ratio: int = 2,
|
||||
up_kernel_size: int = 12,
|
||||
down_kernel_size: int = 12):
|
||||
super().__init__()
|
||||
self.up_ratio = up_ratio
|
||||
self.down_ratio = down_ratio
|
||||
self.act = activation
|
||||
self.upsample = UpSample1d(up_ratio, up_kernel_size)
|
||||
self.downsample = DownSample1d(down_ratio, down_kernel_size)
|
||||
|
||||
# x: [B,C,T]
|
||||
def forward(self, x):
|
||||
x = self.upsample(x)
|
||||
x = self.act(x)
|
||||
x = self.downsample(x)
|
||||
|
||||
return x
|
||||
@ -1,95 +0,0 @@
|
||||
# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import math
|
||||
|
||||
if 'sinc' in dir(torch):
|
||||
sinc = torch.sinc
|
||||
else:
|
||||
# This code is adopted from adefossez's julius.core.sinc under the MIT License
|
||||
# https://adefossez.github.io/julius/julius/core.html
|
||||
# LICENSE is in incl_licenses directory.
|
||||
def sinc(x: torch.Tensor):
|
||||
"""
|
||||
Implementation of sinc, i.e. sin(pi * x) / (pi * x)
|
||||
__Warning__: Different to julius.sinc, the input is multiplied by `pi`!
|
||||
"""
|
||||
return torch.where(x == 0,
|
||||
torch.tensor(1., device=x.device, dtype=x.dtype),
|
||||
torch.sin(math.pi * x) / math.pi / x)
|
||||
|
||||
|
||||
# This code is adopted from adefossez's julius.lowpass.LowPassFilters under the MIT License
|
||||
# https://adefossez.github.io/julius/julius/lowpass.html
|
||||
# LICENSE is in incl_licenses directory.
|
||||
def kaiser_sinc_filter1d(cutoff, half_width, kernel_size): # return filter [1,1,kernel_size]
|
||||
even = (kernel_size % 2 == 0)
|
||||
half_size = kernel_size // 2
|
||||
|
||||
#For kaiser window
|
||||
delta_f = 4 * half_width
|
||||
A = 2.285 * (half_size - 1) * math.pi * delta_f + 7.95
|
||||
if A > 50.:
|
||||
beta = 0.1102 * (A - 8.7)
|
||||
elif A >= 21.:
|
||||
beta = 0.5842 * (A - 21)**0.4 + 0.07886 * (A - 21.)
|
||||
else:
|
||||
beta = 0.
|
||||
window = torch.kaiser_window(kernel_size, beta=beta, periodic=False)
|
||||
|
||||
# ratio = 0.5/cutoff -> 2 * cutoff = 1 / ratio
|
||||
if even:
|
||||
time = (torch.arange(-half_size, half_size) + 0.5)
|
||||
else:
|
||||
time = torch.arange(kernel_size) - half_size
|
||||
if cutoff == 0:
|
||||
filter_ = torch.zeros_like(time)
|
||||
else:
|
||||
filter_ = 2 * cutoff * window * sinc(2 * cutoff * time)
|
||||
# Normalize filter to have sum = 1, otherwise we will have a small leakage
|
||||
# of the constant component in the input signal.
|
||||
filter_ /= filter_.sum()
|
||||
filter = filter_.view(1, 1, kernel_size)
|
||||
|
||||
return filter
|
||||
|
||||
|
||||
class LowPassFilter1d(nn.Module):
|
||||
def __init__(self,
|
||||
cutoff=0.5,
|
||||
half_width=0.6,
|
||||
stride: int = 1,
|
||||
padding: bool = True,
|
||||
padding_mode: str = 'replicate',
|
||||
kernel_size: int = 12):
|
||||
# kernel_size should be even number for stylegan3 setup,
|
||||
# in this implementation, odd number is also possible.
|
||||
super().__init__()
|
||||
if cutoff < -0.:
|
||||
raise ValueError("Minimum cutoff must be larger than zero.")
|
||||
if cutoff > 0.5:
|
||||
raise ValueError("A cutoff above 0.5 does not make sense.")
|
||||
self.kernel_size = kernel_size
|
||||
self.even = (kernel_size % 2 == 0)
|
||||
self.pad_left = kernel_size // 2 - int(self.even)
|
||||
self.pad_right = kernel_size // 2
|
||||
self.stride = stride
|
||||
self.padding = padding
|
||||
self.padding_mode = padding_mode
|
||||
filter = kaiser_sinc_filter1d(cutoff, half_width, kernel_size)
|
||||
self.register_buffer("filter", filter)
|
||||
|
||||
#input [B, C, T]
|
||||
def forward(self, x):
|
||||
_, C, _ = x.shape
|
||||
|
||||
if self.padding:
|
||||
x = F.pad(x, (self.pad_left, self.pad_right),
|
||||
mode=self.padding_mode)
|
||||
out = F.conv1d(x, self.filter.expand(C, -1, -1),
|
||||
stride=self.stride, groups=C)
|
||||
|
||||
return out
|
||||
@ -1,49 +0,0 @@
|
||||
# Adapted from https://github.com/junjun3518/alias-free-torch under the Apache License 2.0
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import torch.nn as nn
|
||||
from torch.nn import functional as F
|
||||
from .filter import LowPassFilter1d
|
||||
from .filter import kaiser_sinc_filter1d
|
||||
|
||||
|
||||
class UpSample1d(nn.Module):
|
||||
def __init__(self, ratio=2, kernel_size=None):
|
||||
super().__init__()
|
||||
self.ratio = ratio
|
||||
self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size
|
||||
self.stride = ratio
|
||||
self.pad = self.kernel_size // ratio - 1
|
||||
self.pad_left = self.pad * self.stride + (self.kernel_size - self.stride) // 2
|
||||
self.pad_right = self.pad * self.stride + (self.kernel_size - self.stride + 1) // 2
|
||||
filter = kaiser_sinc_filter1d(cutoff=0.5 / ratio,
|
||||
half_width=0.6 / ratio,
|
||||
kernel_size=self.kernel_size)
|
||||
self.register_buffer("filter", filter)
|
||||
|
||||
# x: [B, C, T]
|
||||
def forward(self, x):
|
||||
_, C, _ = x.shape
|
||||
|
||||
x = F.pad(x, (self.pad, self.pad), mode='replicate')
|
||||
x = self.ratio * F.conv_transpose1d(
|
||||
x, self.filter.expand(C, -1, -1), stride=self.stride, groups=C)
|
||||
x = x[..., self.pad_left:-self.pad_right]
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class DownSample1d(nn.Module):
|
||||
def __init__(self, ratio=2, kernel_size=None):
|
||||
super().__init__()
|
||||
self.ratio = ratio
|
||||
self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size
|
||||
self.lowpass = LowPassFilter1d(cutoff=0.5 / ratio,
|
||||
half_width=0.6 / ratio,
|
||||
stride=ratio,
|
||||
kernel_size=self.kernel_size)
|
||||
|
||||
def forward(self, x):
|
||||
xx = self.lowpass(x)
|
||||
|
||||
return xx
|
||||
@ -11,7 +11,6 @@ from tortoise.utils.typical_sampling import TypicalLogitsWarper
|
||||
|
||||
from tortoise.utils.device import get_device_count
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
def null_position_embeddings(range, dim):
|
||||
return torch.zeros((range.shape[0], range.shape[1], dim), device=range.device)
|
||||
@ -222,8 +221,7 @@ class ConditioningEncoder(nn.Module):
|
||||
class LearnedPositionEmbeddings(nn.Module):
|
||||
def __init__(self, seq_len, model_dim, init=.02):
|
||||
super().__init__()
|
||||
# ml.Embedding
|
||||
self.emb = ml.Embedding(seq_len, model_dim)
|
||||
self.emb = nn.Embedding(seq_len, model_dim)
|
||||
# Initializing this way is standard for GPT-2
|
||||
self.emb.weight.data.normal_(mean=0.0, std=init)
|
||||
|
||||
@ -232,7 +230,7 @@ class LearnedPositionEmbeddings(nn.Module):
|
||||
return self.emb(torch.arange(0, sl, device=x.device))
|
||||
|
||||
def get_fixed_embedding(self, ind, dev):
|
||||
return self.emb(torch.arange(0, ind, device=dev))[ind-1:ind]
|
||||
return self.emb(torch.tensor([ind], device=dev)).unsqueeze(0)
|
||||
|
||||
|
||||
def build_hf_gpt_transformer(layers, model_dim, heads, max_mel_seq_len, max_text_seq_len, checkpointing):
|
||||
@ -283,9 +281,9 @@ class MelEncoder(nn.Module):
|
||||
|
||||
|
||||
class UnifiedVoice(nn.Module):
|
||||
def __init__(self, layers=8, model_dim=512, heads=8, max_text_tokens=120, max_prompt_tokens=2, max_mel_tokens=250, max_conditioning_inputs=1,
|
||||
def __init__(self, layers=8, model_dim=512, heads=8, max_text_tokens=120, max_mel_tokens=250, max_conditioning_inputs=1,
|
||||
mel_length_compression=1024, number_text_tokens=256,
|
||||
start_text_token=None, stop_text_token=0, number_mel_codes=8194, start_mel_token=8192,
|
||||
start_text_token=None, number_mel_codes=8194, start_mel_token=8192,
|
||||
stop_mel_token=8193, train_solo_embeddings=False, use_mel_codes_as_input=True,
|
||||
checkpointing=True, types=1):
|
||||
"""
|
||||
@ -295,7 +293,6 @@ class UnifiedVoice(nn.Module):
|
||||
heads: Number of transformer heads. Must be divisible by model_dim. Recommend model_dim//64
|
||||
max_text_tokens: Maximum number of text tokens that will be encountered by model.
|
||||
max_mel_tokens: Maximum number of MEL tokens that will be encountered by model.
|
||||
max_prompt_tokens: compat set to 2, 70 for XTTS
|
||||
max_conditioning_inputs: Maximum number of conditioning inputs provided to the model. If (1), conditioning input can be of format (b,80,s), otherwise (b,n,80,s).
|
||||
mel_length_compression: The factor between <number_input_samples> and <mel_tokens>. Used to compute MEL code padding given wav input length.
|
||||
number_text_tokens:
|
||||
@ -312,7 +309,7 @@ class UnifiedVoice(nn.Module):
|
||||
|
||||
self.number_text_tokens = number_text_tokens
|
||||
self.start_text_token = number_text_tokens * types if start_text_token is None else start_text_token
|
||||
self.stop_text_token = stop_text_token
|
||||
self.stop_text_token = 0
|
||||
self.number_mel_codes = number_mel_codes
|
||||
self.start_mel_token = start_mel_token
|
||||
self.stop_mel_token = stop_mel_token
|
||||
@ -320,16 +317,13 @@ class UnifiedVoice(nn.Module):
|
||||
self.heads = heads
|
||||
self.max_mel_tokens = max_mel_tokens
|
||||
self.max_text_tokens = max_text_tokens
|
||||
self.max_prompt_tokens = max_prompt_tokens
|
||||
self.model_dim = model_dim
|
||||
self.max_conditioning_inputs = max_conditioning_inputs
|
||||
self.mel_length_compression = mel_length_compression
|
||||
self.conditioning_encoder = ConditioningEncoder(80, model_dim, num_attn_heads=heads)
|
||||
# ml.Embedding
|
||||
self.text_embedding = ml.Embedding(self.number_text_tokens*types+1, model_dim)
|
||||
self.text_embedding = nn.Embedding(self.number_text_tokens*types+1, model_dim)
|
||||
if use_mel_codes_as_input:
|
||||
# ml.Embedding
|
||||
self.mel_embedding = ml.Embedding(self.number_mel_codes, model_dim)
|
||||
self.mel_embedding = nn.Embedding(self.number_mel_codes, model_dim)
|
||||
else:
|
||||
self.mel_embedding = MelEncoder(model_dim, resblocks_per_reduction=1)
|
||||
self.gpt, self.mel_pos_embedding, self.text_pos_embedding, self.mel_layer_pos_embedding, self.text_layer_pos_embedding = \
|
||||
@ -342,10 +336,8 @@ class UnifiedVoice(nn.Module):
|
||||
self.text_solo_embedding = 0
|
||||
|
||||
self.final_norm = nn.LayerNorm(model_dim)
|
||||
# nn.Linear
|
||||
self.text_head = ml.Linear(model_dim, self.number_text_tokens*types+1)
|
||||
# nn.Linear
|
||||
self.mel_head = ml.Linear(model_dim, self.number_mel_codes)
|
||||
self.text_head = nn.Linear(model_dim, self.number_text_tokens*types+1)
|
||||
self.mel_head = nn.Linear(model_dim, self.number_mel_codes)
|
||||
|
||||
# Initialize the embeddings per the GPT-2 scheme
|
||||
embeddings = [self.text_embedding]
|
||||
@ -354,8 +346,8 @@ class UnifiedVoice(nn.Module):
|
||||
for module in embeddings:
|
||||
module.weight.data.normal_(mean=0.0, std=.02)
|
||||
|
||||
def post_init_gpt2_config(self, use_deepspeed=False, kv_cache=False):
|
||||
seq_length = self.max_mel_tokens + self.max_text_tokens + self.max_prompt_tokens
|
||||
def post_init_gpt2_config(self, kv_cache=False):
|
||||
seq_length = self.max_mel_tokens + self.max_text_tokens + 2
|
||||
gpt_config = GPT2Config(vocab_size=self.max_mel_tokens,
|
||||
n_positions=seq_length,
|
||||
n_ctx=seq_length,
|
||||
@ -365,17 +357,6 @@ class UnifiedVoice(nn.Module):
|
||||
gradient_checkpointing=False,
|
||||
use_cache=True)
|
||||
self.inference_model = GPT2InferenceModel(gpt_config, self.gpt, self.mel_pos_embedding, self.mel_embedding, self.final_norm, self.mel_head, kv_cache=kv_cache)
|
||||
#print(f'use_deepspeed autoregressive_debug {use_deepspeed}')
|
||||
if use_deepspeed and torch.cuda.is_available():
|
||||
import deepspeed
|
||||
self.ds_engine = deepspeed.init_inference(model=self.inference_model,
|
||||
mp_size=1,
|
||||
replace_with_kernel_inject=True,
|
||||
dtype=torch.float32)
|
||||
self.inference_model = self.ds_engine.module.eval()
|
||||
else:
|
||||
self.inference_model = self.inference_model.eval()
|
||||
|
||||
self.gpt.wte = self.mel_embedding
|
||||
|
||||
def build_aligned_inputs_and_targets(self, input, start_token, stop_token):
|
||||
@ -496,9 +477,9 @@ class UnifiedVoice(nn.Module):
|
||||
|
||||
def inference_speech(self, speech_conditioning_latent, text_inputs, input_tokens=None, num_return_sequences=1,
|
||||
max_generate_length=None, typical_sampling=False, typical_mass=.9, **hf_generate_kwargs):
|
||||
seq_length = self.max_mel_tokens + self.max_text_tokens + self.max_prompt_tokens
|
||||
seq_length = self.max_mel_tokens + self.max_text_tokens + 2
|
||||
if not hasattr(self, 'inference_model'):
|
||||
self.post_init_gpt2_config(kv_cache=self.kv_cache)
|
||||
self.post_init_gpt2_config(kv_cache=self.kv_cachepost_init_gpt2_config)
|
||||
|
||||
|
||||
text_inputs = F.pad(text_inputs, (0, 1), value=self.stop_text_token)
|
||||
|
||||
@ -1,485 +0,0 @@
|
||||
# Copyright (c) 2022 NVIDIA CORPORATION.
|
||||
# Licensed under the MIT license.
|
||||
|
||||
# Adapted from https://github.com/jik876/hifi-gan under the MIT license.
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import json
|
||||
import os
|
||||
import torch, torch.utils.data
|
||||
import tortoise.models.activations as activations
|
||||
from torch.nn import Conv1d, ConvTranspose1d, Conv2d
|
||||
from torch.nn.utils import weight_norm, remove_weight_norm, spectral_norm
|
||||
from tortoise.models.alias_free_torch import *
|
||||
from librosa.filters import mel as librosa_mel_fn
|
||||
|
||||
LRELU_SLOPE = 0.1
|
||||
|
||||
|
||||
class AMPBlock1(torch.nn.Module):
|
||||
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5), activation=None):
|
||||
super(AMPBlock1, self).__init__()
|
||||
self.h = h
|
||||
|
||||
self.convs1 = nn.ModuleList([
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]))),
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1]))),
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2],
|
||||
padding=get_padding(kernel_size, dilation[2])))
|
||||
])
|
||||
self.convs1.apply(init_weights)
|
||||
|
||||
self.convs2 = nn.ModuleList([
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
||||
padding=get_padding(kernel_size, 1))),
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
||||
padding=get_padding(kernel_size, 1))),
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
||||
padding=get_padding(kernel_size, 1)))
|
||||
])
|
||||
self.convs2.apply(init_weights)
|
||||
|
||||
self.num_layers = len(self.convs1) + len(self.convs2) # total number of conv layers
|
||||
|
||||
if activation == 'snake': # periodic nonlinearity with snake function and anti-aliasing
|
||||
self.activations = nn.ModuleList([
|
||||
Activation1d(
|
||||
activation=activations.Snake(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
])
|
||||
elif activation == 'snakebeta': # periodic nonlinearity with snakebeta function and anti-aliasing
|
||||
self.activations = nn.ModuleList([
|
||||
Activation1d(
|
||||
activation=activations.SnakeBeta(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'.")
|
||||
|
||||
def forward(self, x):
|
||||
acts1, acts2 = self.activations[::2], self.activations[1::2]
|
||||
for c1, c2, a1, a2 in zip(self.convs1, self.convs2, acts1, acts2):
|
||||
xt = a1(x)
|
||||
xt = c1(xt)
|
||||
xt = a2(xt)
|
||||
xt = c2(xt)
|
||||
x = xt + x
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs1:
|
||||
remove_weight_norm(l)
|
||||
for l in self.convs2:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
class AMPBlock2(torch.nn.Module):
|
||||
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3), activation=None):
|
||||
super(AMPBlock2, self).__init__()
|
||||
self.h = h
|
||||
|
||||
self.convs = nn.ModuleList([
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]))),
|
||||
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1])))
|
||||
])
|
||||
self.convs.apply(init_weights)
|
||||
|
||||
self.num_layers = len(self.convs) # total number of conv layers
|
||||
|
||||
if activation == 'snake': # periodic nonlinearity with snake function and anti-aliasing
|
||||
self.activations = nn.ModuleList([
|
||||
Activation1d(
|
||||
activation=activations.Snake(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
])
|
||||
elif activation == 'snakebeta': # periodic nonlinearity with snakebeta function and anti-aliasing
|
||||
self.activations = nn.ModuleList([
|
||||
Activation1d(
|
||||
activation=activations.SnakeBeta(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'.")
|
||||
|
||||
def forward(self, x):
|
||||
for c, a in zip(self.convs, self.activations):
|
||||
xt = a(x)
|
||||
xt = c(xt)
|
||||
x = xt + x
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
|
||||
class AttrDict(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AttrDict, self).__init__(*args, **kwargs)
|
||||
self.__dict__ = self
|
||||
|
||||
class BigVGAN(nn.Module):
|
||||
# this is our main BigVGAN model. Applies anti-aliased periodic activation for resblocks.
|
||||
def __init__(self, config=None, data=None):
|
||||
super(BigVGAN, self).__init__()
|
||||
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), 'config.json'), 'r') as f:
|
||||
data = f.read()
|
||||
"""
|
||||
if config and data is None:
|
||||
with open(config, 'r') as f:
|
||||
data = f.read()
|
||||
jsonConfig = json.loads(data)
|
||||
elif data is not None:
|
||||
if isinstance(data, str):
|
||||
jsonConfig = json.loads(data)
|
||||
else:
|
||||
jsonConfig = data
|
||||
else:
|
||||
raise Exception("no config specified")
|
||||
|
||||
|
||||
global h
|
||||
h = AttrDict(jsonConfig)
|
||||
|
||||
self.mel_channel = h.num_mels
|
||||
self.noise_dim = h.n_fft
|
||||
self.hop_length = h.hop_size
|
||||
self.num_kernels = len(h.resblock_kernel_sizes)
|
||||
self.num_upsamples = len(h.upsample_rates)
|
||||
|
||||
# pre conv
|
||||
self.conv_pre = weight_norm(Conv1d(h.num_mels, h.upsample_initial_channel, 7, 1, padding=3))
|
||||
|
||||
# define which AMPBlock to use. BigVGAN uses AMPBlock1 as default
|
||||
resblock = AMPBlock1 if h.resblock == '1' else AMPBlock2
|
||||
|
||||
# transposed conv-based upsamplers. does not apply anti-aliasing
|
||||
self.ups = nn.ModuleList()
|
||||
for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)):
|
||||
self.ups.append(nn.ModuleList([
|
||||
weight_norm(ConvTranspose1d(h.upsample_initial_channel // (2 ** i),
|
||||
h.upsample_initial_channel // (2 ** (i + 1)),
|
||||
k, u, padding=(k - u) // 2))
|
||||
]))
|
||||
|
||||
# residual blocks using anti-aliased multi-periodicity composition modules (AMP)
|
||||
self.resblocks = nn.ModuleList()
|
||||
for i in range(len(self.ups)):
|
||||
ch = h.upsample_initial_channel // (2 ** (i + 1))
|
||||
for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)):
|
||||
self.resblocks.append(resblock(h, ch, k, d, activation=h.activation))
|
||||
|
||||
# post conv
|
||||
if h.activation == "snake": # periodic nonlinearity with snake function and anti-aliasing
|
||||
activation_post = activations.Snake(ch, alpha_logscale=h.snake_logscale)
|
||||
self.activation_post = Activation1d(activation=activation_post)
|
||||
elif h.activation == "snakebeta": # periodic nonlinearity with snakebeta function and anti-aliasing
|
||||
activation_post = activations.SnakeBeta(ch, alpha_logscale=h.snake_logscale)
|
||||
self.activation_post = Activation1d(activation=activation_post)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'.")
|
||||
|
||||
self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3))
|
||||
|
||||
# weight initialization
|
||||
for i in range(len(self.ups)):
|
||||
self.ups[i].apply(init_weights)
|
||||
self.conv_post.apply(init_weights)
|
||||
|
||||
def forward(self,x, c):
|
||||
# pre conv
|
||||
x = self.conv_pre(x)
|
||||
|
||||
for i in range(self.num_upsamples):
|
||||
# upsampling
|
||||
for i_up in range(len(self.ups[i])):
|
||||
x = self.ups[i][i_up](x)
|
||||
# AMP blocks
|
||||
xs = None
|
||||
for j in range(self.num_kernels):
|
||||
if xs is None:
|
||||
xs = self.resblocks[i * self.num_kernels + j](x)
|
||||
else:
|
||||
xs += self.resblocks[i * self.num_kernels + j](x)
|
||||
x = xs / self.num_kernels
|
||||
|
||||
# post conv
|
||||
x = self.activation_post(x)
|
||||
x = self.conv_post(x)
|
||||
x = torch.tanh(x)
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
print('Removing weight norm...')
|
||||
for l in self.ups:
|
||||
for l_i in l:
|
||||
remove_weight_norm(l_i)
|
||||
for l in self.resblocks:
|
||||
l.remove_weight_norm()
|
||||
remove_weight_norm(self.conv_pre)
|
||||
remove_weight_norm(self.conv_post)
|
||||
|
||||
def inference(self, c, z=None):
|
||||
# pad input mel with zeros to cut artifact
|
||||
# see https://github.com/seungwonpark/melgan/issues/8
|
||||
zero = torch.full((c.shape[0], h.num_mels, 10), -11.5129).to(c.device)
|
||||
mel = torch.cat((c, zero), dim=2)
|
||||
|
||||
if z is None:
|
||||
z = torch.randn(c.shape[0], self.noise_dim, mel.size(2)).to(mel.device)
|
||||
|
||||
audio = self.forward(mel, z)
|
||||
audio = audio[:, :, :-(self.hop_length * 10)]
|
||||
audio = audio.clamp(min=-1, max=1)
|
||||
return audio
|
||||
|
||||
def eval(self, inference=False):
|
||||
super(BigVGAN, self).eval()
|
||||
# don't remove weight norm while validation in training loop
|
||||
if inference:
|
||||
self.remove_weight_norm()
|
||||
|
||||
|
||||
class DiscriminatorP(nn.Module):
|
||||
def __init__(self, h, period, kernel_size=5, stride=3, use_spectral_norm=False):
|
||||
super(DiscriminatorP, self).__init__()
|
||||
self.period = period
|
||||
self.d_mult = h.discriminator_channel_mult
|
||||
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
|
||||
self.convs = nn.ModuleList([
|
||||
norm_f(Conv2d(1, int(32 * self.d_mult), (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(int(32 * self.d_mult), int(128 * self.d_mult), (kernel_size, 1), (stride, 1),
|
||||
padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(int(128 * self.d_mult), int(512 * self.d_mult), (kernel_size, 1), (stride, 1),
|
||||
padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(int(512 * self.d_mult), int(1024 * self.d_mult), (kernel_size, 1), (stride, 1),
|
||||
padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(int(1024 * self.d_mult), int(1024 * self.d_mult), (kernel_size, 1), 1, padding=(2, 0))),
|
||||
])
|
||||
self.conv_post = norm_f(Conv2d(int(1024 * self.d_mult), 1, (3, 1), 1, padding=(1, 0)))
|
||||
|
||||
def forward(self, x):
|
||||
fmap = []
|
||||
|
||||
# 1d to 2d
|
||||
b, c, t = x.shape
|
||||
if t % self.period != 0: # pad first
|
||||
n_pad = self.period - (t % self.period)
|
||||
x = F.pad(x, (0, n_pad), "reflect")
|
||||
t = t + n_pad
|
||||
x = x.view(b, c, t // self.period, self.period)
|
||||
|
||||
for l in self.convs:
|
||||
x = l(x)
|
||||
x = F.leaky_relu(x, LRELU_SLOPE)
|
||||
fmap.append(x)
|
||||
x = self.conv_post(x)
|
||||
fmap.append(x)
|
||||
x = torch.flatten(x, 1, -1)
|
||||
|
||||
return x, fmap
|
||||
|
||||
|
||||
class MultiPeriodDiscriminator(nn.Module):
|
||||
def __init__(self, h):
|
||||
super(MultiPeriodDiscriminator, self).__init__()
|
||||
self.mpd_reshapes = h.mpd_reshapes
|
||||
print("mpd_reshapes: {}".format(self.mpd_reshapes))
|
||||
discriminators = [DiscriminatorP(h, rs, use_spectral_norm=h.use_spectral_norm) for rs in self.mpd_reshapes]
|
||||
self.discriminators = nn.ModuleList(discriminators)
|
||||
|
||||
def forward(self, y, y_hat):
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
for i, d in enumerate(self.discriminators):
|
||||
y_d_r, fmap_r = d(y)
|
||||
y_d_g, fmap_g = d(y_hat)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
class DiscriminatorR(nn.Module):
|
||||
def __init__(self, cfg, resolution):
|
||||
super().__init__()
|
||||
|
||||
self.resolution = resolution
|
||||
assert len(self.resolution) == 3, \
|
||||
"MRD layer requires list with len=3, got {}".format(self.resolution)
|
||||
self.lrelu_slope = LRELU_SLOPE
|
||||
|
||||
norm_f = weight_norm if cfg.use_spectral_norm == False else spectral_norm
|
||||
if hasattr(cfg, "mrd_use_spectral_norm"):
|
||||
print("INFO: overriding MRD use_spectral_norm as {}".format(cfg.mrd_use_spectral_norm))
|
||||
norm_f = weight_norm if cfg.mrd_use_spectral_norm == False else spectral_norm
|
||||
self.d_mult = cfg.discriminator_channel_mult
|
||||
if hasattr(cfg, "mrd_channel_mult"):
|
||||
print("INFO: overriding mrd channel multiplier as {}".format(cfg.mrd_channel_mult))
|
||||
self.d_mult = cfg.mrd_channel_mult
|
||||
|
||||
self.convs = nn.ModuleList([
|
||||
norm_f(nn.Conv2d(1, int(32 * self.d_mult), (3, 9), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(int(32 * self.d_mult), int(32 * self.d_mult), (3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(int(32 * self.d_mult), int(32 * self.d_mult), (3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(int(32 * self.d_mult), int(32 * self.d_mult), (3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(int(32 * self.d_mult), int(32 * self.d_mult), (3, 3), padding=(1, 1))),
|
||||
])
|
||||
self.conv_post = norm_f(nn.Conv2d(int(32 * self.d_mult), 1, (3, 3), padding=(1, 1)))
|
||||
|
||||
def forward(self, x):
|
||||
fmap = []
|
||||
|
||||
x = self.spectrogram(x)
|
||||
x = x.unsqueeze(1)
|
||||
for l in self.convs:
|
||||
x = l(x)
|
||||
x = F.leaky_relu(x, self.lrelu_slope)
|
||||
fmap.append(x)
|
||||
x = self.conv_post(x)
|
||||
fmap.append(x)
|
||||
x = torch.flatten(x, 1, -1)
|
||||
|
||||
return x, fmap
|
||||
|
||||
def spectrogram(self, x):
|
||||
n_fft, hop_length, win_length = self.resolution
|
||||
x = F.pad(x, (int((n_fft - hop_length) / 2), int((n_fft - hop_length) / 2)), mode='reflect')
|
||||
x = x.squeeze(1)
|
||||
x = torch.stft(x, n_fft=n_fft, hop_length=hop_length, win_length=win_length, center=False, return_complex=True)
|
||||
x = torch.view_as_real(x) # [B, F, TT, 2]
|
||||
mag = torch.norm(x, p=2, dim=-1) # [B, F, TT]
|
||||
|
||||
return mag
|
||||
|
||||
|
||||
class MultiResolutionDiscriminator(nn.Module):
|
||||
def __init__(self, cfg, debug=False):
|
||||
super().__init__()
|
||||
self.resolutions = cfg.resolutions
|
||||
assert len(self.resolutions) == 3, \
|
||||
"MRD requires list of list with len=3, each element having a list with len=3. got {}". \
|
||||
format(self.resolutions)
|
||||
self.discriminators = nn.ModuleList(
|
||||
[DiscriminatorR(cfg, resolution) for resolution in self.resolutions]
|
||||
)
|
||||
|
||||
def forward(self, y, y_hat):
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
|
||||
for i, d in enumerate(self.discriminators):
|
||||
y_d_r, fmap_r = d(x=y)
|
||||
y_d_g, fmap_g = d(x=y_hat)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
def get_mel(x):
|
||||
return mel_spectrogram(x, h.n_fft, h.num_mels, h.sampling_rate, h.hop_size, h.win_size, h.fmin, h.fmax)
|
||||
|
||||
def mel_spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False):
|
||||
if torch.min(y) < -1.:
|
||||
print('min value is ', torch.min(y))
|
||||
if torch.max(y) > 1.:
|
||||
print('max value is ', torch.max(y))
|
||||
|
||||
global mel_basis, hann_window
|
||||
if fmax not in mel_basis:
|
||||
mel = librosa_mel_fn(sampling_rate, n_fft, num_mels, fmin, fmax)
|
||||
mel_basis[str(fmax)+'_'+str(y.device)] = torch.from_numpy(mel).float().to(y.device)
|
||||
hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device)
|
||||
|
||||
y = torch.nn.functional.pad(y.unsqueeze(1), (int((n_fft-hop_size)/2), int((n_fft-hop_size)/2)), mode='reflect')
|
||||
y = y.squeeze(1)
|
||||
|
||||
# complex tensor as default, then use view_as_real for future pytorch compatibility
|
||||
spec = torch.stft(y, n_fft, hop_length=hop_size, win_length=win_size, window=hann_window[str(y.device)],
|
||||
center=center, pad_mode='reflect', normalized=False, onesided=True, return_complex=True)
|
||||
spec = torch.view_as_real(spec)
|
||||
spec = torch.sqrt(spec.pow(2).sum(-1)+(1e-9))
|
||||
|
||||
spec = torch.matmul(mel_basis[str(fmax)+'_'+str(y.device)], spec)
|
||||
spec = torch.nn.utils.spectral_normalize_torch(spec)
|
||||
|
||||
return spec
|
||||
|
||||
def feature_loss(fmap_r, fmap_g):
|
||||
loss = 0
|
||||
for dr, dg in zip(fmap_r, fmap_g):
|
||||
for rl, gl in zip(dr, dg):
|
||||
loss += torch.mean(torch.abs(rl - gl))
|
||||
|
||||
return loss * 2
|
||||
|
||||
|
||||
def init_weights(m, mean=0.0, std=0.01):
|
||||
classname = m.__class__.__name__
|
||||
if classname.find("Conv") != -1:
|
||||
m.weight.data.normal_(mean, std)
|
||||
|
||||
|
||||
def get_padding(kernel_size, dilation=1):
|
||||
return int((kernel_size * dilation - dilation) / 2)
|
||||
|
||||
|
||||
def discriminator_loss(disc_real_outputs, disc_generated_outputs):
|
||||
loss = 0
|
||||
r_losses = []
|
||||
g_losses = []
|
||||
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
|
||||
r_loss = torch.mean((1 - dr) ** 2)
|
||||
g_loss = torch.mean(dg ** 2)
|
||||
loss += (r_loss + g_loss)
|
||||
r_losses.append(r_loss.item())
|
||||
g_losses.append(g_loss.item())
|
||||
|
||||
return loss, r_losses, g_losses
|
||||
|
||||
|
||||
def generator_loss(disc_outputs):
|
||||
loss = 0
|
||||
gen_losses = []
|
||||
for dg in disc_outputs:
|
||||
l = torch.mean((1 - dg) ** 2)
|
||||
gen_losses.append(l)
|
||||
loss += l
|
||||
|
||||
return loss, gen_losses
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
model = BigVGAN()
|
||||
|
||||
c = torch.randn(3, 100, 10)
|
||||
z = torch.randn(3, 64, 10)
|
||||
print(c.shape)
|
||||
|
||||
y = model(c, z)
|
||||
print(y.shape)
|
||||
assert y.shape == torch.Size([3, 1, 2560])
|
||||
|
||||
pytorch_total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
||||
print(pytorch_total_params)
|
||||
@ -3,7 +3,6 @@ import torch.nn as nn
|
||||
|
||||
from tortoise.models.arch_util import Upsample, Downsample, normalization, zero_module, AttentionBlock
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
class ResBlock(nn.Module):
|
||||
def __init__(
|
||||
@ -125,8 +124,7 @@ class AudioMiniEncoderWithClassifierHead(nn.Module):
|
||||
def __init__(self, classes, distribute_zero_label=True, **kwargs):
|
||||
super().__init__()
|
||||
self.enc = AudioMiniEncoder(**kwargs)
|
||||
# nn.Linear
|
||||
self.head = ml.Linear(self.enc.dim, classes)
|
||||
self.head = nn.Linear(self.enc.dim, classes)
|
||||
self.num_classes = classes
|
||||
self.distribute_zero_label = distribute_zero_label
|
||||
|
||||
|
||||
@ -7,9 +7,6 @@ from tortoise.models.arch_util import CheckpointedXTransformerEncoder
|
||||
from tortoise.models.transformer import Transformer
|
||||
from tortoise.models.xtransformers import Encoder
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
from tortoise.utils.device import print_stats, do_gc
|
||||
|
||||
def exists(val):
|
||||
return val is not None
|
||||
@ -47,15 +44,11 @@ class CLVP(nn.Module):
|
||||
use_xformers=False,
|
||||
):
|
||||
super().__init__()
|
||||
# nn.Embedding
|
||||
self.text_emb = ml.Embedding(num_text_tokens, dim_text)
|
||||
# nn.Linear
|
||||
self.to_text_latent = ml.Linear(dim_text, dim_latent, bias=False)
|
||||
self.text_emb = nn.Embedding(num_text_tokens, dim_text)
|
||||
self.to_text_latent = nn.Linear(dim_text, dim_latent, bias=False)
|
||||
|
||||
# nn.Embedding
|
||||
self.speech_emb = ml.Embedding(num_speech_tokens, dim_speech)
|
||||
# nn.Linear
|
||||
self.to_speech_latent = ml.Linear(dim_speech, dim_latent, bias=False)
|
||||
self.speech_emb = nn.Embedding(num_speech_tokens, dim_speech)
|
||||
self.to_speech_latent = nn.Linear(dim_speech, dim_latent, bias=False)
|
||||
|
||||
if use_xformers:
|
||||
self.text_transformer = CheckpointedXTransformerEncoder(
|
||||
@ -100,10 +93,8 @@ class CLVP(nn.Module):
|
||||
self.wav_token_compression = wav_token_compression
|
||||
self.xformers = use_xformers
|
||||
if not use_xformers:
|
||||
# nn.Embedding
|
||||
self.text_pos_emb = ml.Embedding(text_seq_len, dim_text)
|
||||
# nn.Embedding
|
||||
self.speech_pos_emb = ml.Embedding(num_speech_tokens, dim_speech)
|
||||
self.text_pos_emb = nn.Embedding(text_seq_len, dim_text)
|
||||
self.speech_pos_emb = nn.Embedding(num_speech_tokens, dim_speech)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
@ -126,13 +117,14 @@ class CLVP(nn.Module):
|
||||
text_emb += self.text_pos_emb(torch.arange(text.shape[1], device=device))
|
||||
speech_emb += self.speech_pos_emb(torch.arange(speech_emb.shape[1], device=device))
|
||||
|
||||
|
||||
text_latents = self.to_text_latent(masked_mean(self.text_transformer(text_emb, mask=text_mask), text_mask, dim=1))
|
||||
enc_text = self.text_transformer(text_emb, mask=text_mask)
|
||||
enc_speech = self.speech_transformer(speech_emb, mask=voice_mask)
|
||||
|
||||
# on ROCm at least, allocated VRAM spikes here
|
||||
do_gc()
|
||||
speech_latents = self.to_speech_latent(masked_mean(self.speech_transformer(speech_emb, mask=voice_mask), voice_mask, dim=1))
|
||||
do_gc()
|
||||
text_latents = masked_mean(enc_text, text_mask, dim=1)
|
||||
speech_latents = masked_mean(enc_speech, voice_mask, dim=1)
|
||||
|
||||
text_latents = self.to_text_latent(text_latents)
|
||||
speech_latents = self.to_speech_latent(speech_latents)
|
||||
|
||||
text_latents, speech_latents = map(lambda t: F.normalize(t, p=2, dim=-1), (text_latents, speech_latents))
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ from torch import einsum
|
||||
from tortoise.models.arch_util import AttentionBlock
|
||||
from tortoise.models.xtransformers import ContinuousTransformerWrapper, Encoder
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
def exists(val):
|
||||
return val is not None
|
||||
@ -55,8 +54,7 @@ class CollapsingTransformer(nn.Module):
|
||||
class ConvFormatEmbedding(nn.Module):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
# nn.Embedding
|
||||
self.emb = ml.Embedding(*args, **kwargs)
|
||||
self.emb = nn.Embedding(*args, **kwargs)
|
||||
|
||||
def forward(self, x):
|
||||
y = self.emb(x)
|
||||
@ -85,8 +83,7 @@ class CVVP(nn.Module):
|
||||
nn.Conv1d(model_dim//2, model_dim, kernel_size=3, stride=2, padding=1))
|
||||
self.conditioning_transformer = CollapsingTransformer(
|
||||
model_dim, model_dim, transformer_heads, dropout, conditioning_enc_depth, cond_mask_percentage)
|
||||
# nn.Linear
|
||||
self.to_conditioning_latent = ml.Linear(
|
||||
self.to_conditioning_latent = nn.Linear(
|
||||
latent_dim, latent_dim, bias=False)
|
||||
|
||||
if mel_codes is None:
|
||||
@ -96,8 +93,7 @@ class CVVP(nn.Module):
|
||||
self.speech_emb = ConvFormatEmbedding(mel_codes, model_dim)
|
||||
self.speech_transformer = CollapsingTransformer(
|
||||
model_dim, latent_dim, transformer_heads, dropout, speech_enc_depth, speech_mask_percentage)
|
||||
# nn.Linear
|
||||
self.to_speech_latent = ml.Linear(
|
||||
self.to_speech_latent = nn.Linear(
|
||||
latent_dim, latent_dim, bias=False)
|
||||
|
||||
def get_grad_norm_parameter_groups(self):
|
||||
|
||||
@ -10,8 +10,6 @@ from torch import autocast
|
||||
from tortoise.models.arch_util import normalization, AttentionBlock
|
||||
from tortoise.utils.device import get_device_name
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
def is_latent(t):
|
||||
return t.dtype == torch.float
|
||||
|
||||
@ -89,8 +87,7 @@ class ResBlock(TimestepBlock):
|
||||
|
||||
self.emb_layers = nn.Sequential(
|
||||
nn.SiLU(),
|
||||
# nn.Linear
|
||||
ml.Linear(
|
||||
nn.Linear(
|
||||
emb_channels,
|
||||
2 * self.out_channels if use_scale_shift_norm else self.out_channels,
|
||||
),
|
||||
@ -163,19 +160,16 @@ class DiffusionTts(nn.Module):
|
||||
|
||||
self.inp_block = nn.Conv1d(in_channels, model_channels, 3, 1, 1)
|
||||
self.time_embed = nn.Sequential(
|
||||
# nn.Linear
|
||||
ml.Linear(model_channels, model_channels),
|
||||
nn.Linear(model_channels, model_channels),
|
||||
nn.SiLU(),
|
||||
# nn.Linear
|
||||
ml.Linear(model_channels, model_channels),
|
||||
nn.Linear(model_channels, model_channels),
|
||||
)
|
||||
|
||||
# Either code_converter or latent_converter is used, depending on what type of conditioning data is fed.
|
||||
# This model is meant to be able to be trained on both for efficiency purposes - it is far less computationally
|
||||
# complex to generate tokens, while generating latents will normally mean propagating through a deep autoregressive
|
||||
# transformer network.
|
||||
# nn.Embedding
|
||||
self.code_embedding = ml.Embedding(in_tokens, model_channels)
|
||||
self.code_embedding = nn.Embedding(in_tokens, model_channels)
|
||||
self.code_converter = nn.Sequential(
|
||||
AttentionBlock(model_channels, num_heads, relative_pos_embeddings=True),
|
||||
AttentionBlock(model_channels, num_heads, relative_pos_embeddings=True),
|
||||
|
||||
@ -4,7 +4,6 @@ import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
def fused_leaky_relu(input, bias=None, negative_slope=0.2, scale=2 ** 0.5):
|
||||
if bias is not None:
|
||||
@ -42,8 +41,7 @@ class RandomLatentConverter(nn.Module):
|
||||
def __init__(self, channels):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(*[EqualLinear(channels, channels, lr_mul=.1) for _ in range(5)],
|
||||
# nn.Linear
|
||||
ml.Linear(channels, channels))
|
||||
nn.Linear(channels, channels))
|
||||
self.channels = channels
|
||||
|
||||
def forward(self, ref):
|
||||
|
||||
@ -6,7 +6,6 @@ from einops import rearrange
|
||||
from rotary_embedding_torch import RotaryEmbedding, broadcat
|
||||
from torch import nn
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
# helpers
|
||||
|
||||
@ -121,12 +120,10 @@ class FeedForward(nn.Module):
|
||||
def __init__(self, dim, dropout = 0., mult = 4.):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
# nn.Linear
|
||||
ml.Linear(dim, dim * mult * 2),
|
||||
nn.Linear(dim, dim * mult * 2),
|
||||
GEGLU(),
|
||||
nn.Dropout(dropout),
|
||||
# nn.Linear
|
||||
ml.Linear(dim * mult, dim)
|
||||
nn.Linear(dim * mult, dim)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
@ -145,11 +142,9 @@ class Attention(nn.Module):
|
||||
|
||||
self.causal = causal
|
||||
|
||||
# nn.Linear
|
||||
self.to_qkv = ml.Linear(dim, inner_dim * 3, bias = False)
|
||||
self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)
|
||||
self.to_out = nn.Sequential(
|
||||
# nn.Linear
|
||||
ml.Linear(inner_dim, dim),
|
||||
nn.Linear(inner_dim, dim),
|
||||
nn.Dropout(dropout)
|
||||
)
|
||||
|
||||
|
||||
@ -8,8 +8,6 @@ import torch.nn.functional as F
|
||||
from einops import rearrange, repeat
|
||||
from torch import nn, einsum
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
DEFAULT_DIM_HEAD = 64
|
||||
|
||||
Intermediates = namedtuple('Intermediates', [
|
||||
@ -123,8 +121,7 @@ class AbsolutePositionalEmbedding(nn.Module):
|
||||
def __init__(self, dim, max_seq_len):
|
||||
super().__init__()
|
||||
self.scale = dim ** -0.5
|
||||
# nn.Embedding
|
||||
self.emb = ml.Embedding(max_seq_len, dim)
|
||||
self.emb = nn.Embedding(max_seq_len, dim)
|
||||
|
||||
def forward(self, x):
|
||||
n = torch.arange(x.shape[1], device=x.device)
|
||||
@ -153,8 +150,7 @@ class RelativePositionBias(nn.Module):
|
||||
self.causal = causal
|
||||
self.num_buckets = num_buckets
|
||||
self.max_distance = max_distance
|
||||
# nn.Embedding
|
||||
self.relative_attention_bias = ml.Embedding(num_buckets, heads)
|
||||
self.relative_attention_bias = nn.Embedding(num_buckets, heads)
|
||||
|
||||
@staticmethod
|
||||
def _relative_position_bucket(relative_position, causal=True, num_buckets=32, max_distance=128):
|
||||
@ -354,8 +350,7 @@ class RMSScaleShiftNorm(nn.Module):
|
||||
self.scale = dim ** -0.5
|
||||
self.eps = eps
|
||||
self.g = nn.Parameter(torch.ones(dim))
|
||||
# nn.Linear
|
||||
self.scale_shift_process = ml.Linear(dim * 2, dim * 2)
|
||||
self.scale_shift_process = nn.Linear(dim * 2, dim * 2)
|
||||
|
||||
def forward(self, x, norm_scale_shift_inp):
|
||||
norm = torch.norm(x, dim=-1, keepdim=True) * self.scale
|
||||
@ -435,8 +430,7 @@ class GLU(nn.Module):
|
||||
def __init__(self, dim_in, dim_out, activation):
|
||||
super().__init__()
|
||||
self.act = activation
|
||||
# nn.Linear
|
||||
self.proj = ml.Linear(dim_in, dim_out * 2)
|
||||
self.proj = nn.Linear(dim_in, dim_out * 2)
|
||||
|
||||
def forward(self, x):
|
||||
x, gate = self.proj(x).chunk(2, dim=-1)
|
||||
@ -461,8 +455,7 @@ class FeedForward(nn.Module):
|
||||
activation = ReluSquared() if relu_squared else nn.GELU()
|
||||
|
||||
project_in = nn.Sequential(
|
||||
# nn.Linear
|
||||
ml.Linear(dim, inner_dim),
|
||||
nn.Linear(dim, inner_dim),
|
||||
activation
|
||||
) if not glu else GLU(dim, inner_dim, activation)
|
||||
|
||||
@ -470,8 +463,7 @@ class FeedForward(nn.Module):
|
||||
project_in,
|
||||
nn.LayerNorm(inner_dim) if post_act_ln else nn.Identity(),
|
||||
nn.Dropout(dropout),
|
||||
# nn.Linear
|
||||
ml.Linear(inner_dim, dim_out)
|
||||
nn.Linear(inner_dim, dim_out)
|
||||
)
|
||||
|
||||
# init last linear layer to 0
|
||||
@ -524,20 +516,16 @@ class Attention(nn.Module):
|
||||
qk_dim = int(collab_compression * qk_dim)
|
||||
self.collab_mixing = nn.Parameter(torch.randn(heads, qk_dim))
|
||||
|
||||
# nn.Linear
|
||||
self.to_q = ml.Linear(dim, qk_dim, bias=False)
|
||||
# nn.Linear
|
||||
self.to_k = ml.Linear(dim, qk_dim, bias=False)
|
||||
# nn.Linear
|
||||
self.to_v = ml.Linear(dim, v_dim, bias=False)
|
||||
self.to_q = nn.Linear(dim, qk_dim, bias=False)
|
||||
self.to_k = nn.Linear(dim, qk_dim, bias=False)
|
||||
self.to_v = nn.Linear(dim, v_dim, bias=False)
|
||||
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
|
||||
# add GLU gating for aggregated values, from alphafold2
|
||||
self.to_v_gate = None
|
||||
if gate_values:
|
||||
# nn.Linear
|
||||
self.to_v_gate = ml.Linear(dim, v_dim)
|
||||
self.to_v_gate = nn.Linear(dim, v_dim)
|
||||
nn.init.constant_(self.to_v_gate.weight, 0)
|
||||
nn.init.constant_(self.to_v_gate.bias, 1)
|
||||
|
||||
@ -573,8 +561,7 @@ class Attention(nn.Module):
|
||||
|
||||
# attention on attention
|
||||
self.attn_on_attn = on_attn
|
||||
# nn.Linear
|
||||
self.to_out = nn.Sequential(ml.Linear(v_dim, dim * 2), nn.GLU()) if on_attn else ml.Linear(v_dim, dim)
|
||||
self.to_out = nn.Sequential(nn.Linear(v_dim, dim * 2), nn.GLU()) if on_attn else nn.Linear(v_dim, dim)
|
||||
|
||||
self.rel_pos_bias = rel_pos_bias
|
||||
if rel_pos_bias:
|
||||
@ -1064,8 +1051,7 @@ class ViTransformerWrapper(nn.Module):
|
||||
self.patch_size = patch_size
|
||||
|
||||
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
|
||||
# nn.Linear
|
||||
self.patch_to_embedding = ml.Linear(patch_dim, dim)
|
||||
self.patch_to_embedding = nn.Linear(patch_dim, dim)
|
||||
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
|
||||
self.dropout = nn.Dropout(emb_dropout)
|
||||
|
||||
@ -1123,21 +1109,18 @@ class TransformerWrapper(nn.Module):
|
||||
self.max_mem_len = max_mem_len
|
||||
self.shift_mem_down = shift_mem_down
|
||||
|
||||
# nn.Embedding
|
||||
self.token_emb = ml.Embedding(num_tokens, emb_dim)
|
||||
self.token_emb = nn.Embedding(num_tokens, emb_dim)
|
||||
self.pos_emb = AbsolutePositionalEmbedding(emb_dim, max_seq_len) if (
|
||||
use_pos_emb and not attn_layers.has_pos_emb) else always(0)
|
||||
self.emb_dropout = nn.Dropout(emb_dropout)
|
||||
|
||||
# nn.Linear
|
||||
self.project_emb = ml.Linear(emb_dim, dim) if emb_dim != dim else nn.Identity()
|
||||
self.project_emb = nn.Linear(emb_dim, dim) if emb_dim != dim else nn.Identity()
|
||||
self.attn_layers = attn_layers
|
||||
self.norm = nn.LayerNorm(dim)
|
||||
|
||||
self.init_()
|
||||
|
||||
# nn.Linear
|
||||
self.to_logits = ml.Linear(dim, num_tokens) if not tie_embedding else lambda t: t @ self.token_emb.weight.t()
|
||||
self.to_logits = nn.Linear(dim, num_tokens) if not tie_embedding else lambda t: t @ self.token_emb.weight.t()
|
||||
|
||||
# memory tokens (like [cls]) from Memory Transformers paper
|
||||
num_memory_tokens = default(num_memory_tokens, 0)
|
||||
@ -1224,14 +1207,12 @@ class ContinuousTransformerWrapper(nn.Module):
|
||||
use_pos_emb and not attn_layers.has_pos_emb) else always(0)
|
||||
self.emb_dropout = nn.Dropout(emb_dropout)
|
||||
|
||||
# nn.Linear
|
||||
self.project_in = ml.Linear(dim_in, dim) if exists(dim_in) else nn.Identity()
|
||||
self.project_in = nn.Linear(dim_in, dim) if exists(dim_in) else nn.Identity()
|
||||
|
||||
self.attn_layers = attn_layers
|
||||
self.norm = nn.LayerNorm(dim)
|
||||
|
||||
# nn.Linear
|
||||
self.project_out = ml.Linear(dim, dim_out) if exists(dim_out) else nn.Identity()
|
||||
self.project_out = nn.Linear(dim, dim_out) if exists(dim_out) else nn.Identity()
|
||||
|
||||
def forward(
|
||||
self,
|
||||
|
||||
@ -17,7 +17,6 @@ if __name__ == '__main__':
|
||||
'Use the & character to join two voices together. Use a comma to perform inference on multiple voices.', default='pat')
|
||||
parser.add_argument('--output_path', type=str, help='Where to store outputs.', default='results/longform/')
|
||||
parser.add_argument('--preset', type=str, help='Which voice preset to use.', default='standard')
|
||||
parser.add_argument('--use_deepspeed', type=bool, help='Use deepspeed for speed bump.', default=True)
|
||||
parser.add_argument('--regenerate', type=str, help='Comma-separated list of clip numbers to re-generate, or nothing.', default=None)
|
||||
parser.add_argument('--candidates', type=int, help='How many output candidates to produce per-voice. Only the first candidate is actually used in the final product, the others can be used manually.', default=1)
|
||||
parser.add_argument('--model_dir', type=str, help='Where to find pretrained model checkpoints. Tortoise automatically downloads these to .models, so this'
|
||||
@ -26,7 +25,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--produce_debug_state', type=bool, help='Whether or not to produce debug_state.pth, which can aid in reproducing problems. Defaults to true.', default=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
tts = TextToSpeech(models_dir=args.model_dir, use_deepspeed=args.use_deepspeed)
|
||||
tts = TextToSpeech(models_dir=args.model_dir)
|
||||
|
||||
outpath = args.output_path
|
||||
selected_voices = args.voice.split(',')
|
||||
|
||||
@ -2,7 +2,6 @@ import os
|
||||
from glob import glob
|
||||
|
||||
import librosa
|
||||
import soundfile as sf
|
||||
import torch
|
||||
import torchaudio
|
||||
import numpy as np
|
||||
@ -10,24 +9,41 @@ from scipy.io.wavfile import read
|
||||
|
||||
from tortoise.utils.stft import STFT
|
||||
|
||||
|
||||
if 'TORTOISE_VOICES_DIR' not in os.environ:
|
||||
voice_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../voices')
|
||||
|
||||
if not os.path.exists(voice_dir):
|
||||
voice_dir = os.path.dirname('./voices/')
|
||||
|
||||
os.environ['TORTOISE_VOICES_DIR'] = voice_dir
|
||||
|
||||
BUILTIN_VOICES_DIR = os.environ.get('TORTOISE_VOICES_DIR')
|
||||
|
||||
os.makedirs(BUILTIN_VOICES_DIR, exist_ok=True)
|
||||
|
||||
def get_voice_dir():
|
||||
target = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../voices')
|
||||
if not os.path.exists(target):
|
||||
target = os.path.dirname('./voices/')
|
||||
return BUILTIN_VOICES_DIR
|
||||
|
||||
os.makedirs(target, exist_ok=True)
|
||||
def load_wav_to_torch(full_path):
|
||||
sampling_rate, data = read(full_path)
|
||||
if data.dtype == np.int32:
|
||||
norm_fix = 2 ** 31
|
||||
elif data.dtype == np.int16:
|
||||
norm_fix = 2 ** 15
|
||||
elif data.dtype == np.float16 or data.dtype == np.float32:
|
||||
norm_fix = 1.
|
||||
else:
|
||||
raise NotImplemented(f"Provided data dtype not supported: {data.dtype}")
|
||||
return (torch.FloatTensor(data.astype(np.float32)) / norm_fix, sampling_rate)
|
||||
|
||||
return target
|
||||
|
||||
def load_audio(audiopath, sampling_rate):
|
||||
if audiopath[-4:] == '.wav':
|
||||
audio, lsr = torchaudio.load(audiopath)
|
||||
audio, lsr = load_wav_to_torch(audiopath)
|
||||
elif audiopath[-4:] == '.mp3':
|
||||
audio, lsr = librosa.load(audiopath, sr=sampling_rate)
|
||||
audio = torch.FloatTensor(audio)
|
||||
elif audiopath[-5:] == '.flac':
|
||||
audio, lsr = sf.read(audiopath)
|
||||
audio = torch.FloatTensor(audio)
|
||||
else:
|
||||
assert False, f"Unsupported audio format provided: {audiopath[-4:]}"
|
||||
|
||||
@ -81,102 +97,37 @@ def dynamic_range_decompression(x, C=1):
|
||||
return torch.exp(x) / C
|
||||
|
||||
|
||||
def get_voices(extra_voice_dirs=[], load_latents=True):
|
||||
dirs = [get_voice_dir()] + extra_voice_dirs
|
||||
def get_voices(extra_voice_dirs=[]):
|
||||
dirs = [BUILTIN_VOICES_DIR] + extra_voice_dirs
|
||||
voices = {}
|
||||
for d in dirs:
|
||||
subs = os.listdir(d)
|
||||
for sub in subs:
|
||||
subj = os.path.join(d, sub)
|
||||
if os.path.isdir(subj):
|
||||
voices[sub] = list(glob(f'{subj}/*.wav')) + list(glob(f'{subj}/*.mp3')) + list(glob(f'{subj}/*.flac'))
|
||||
if load_latents:
|
||||
voices[sub] = voices[sub] + list(glob(f'{subj}/*.pth'))
|
||||
voices[sub] = list(glob(f'{subj}/*.wav')) + list(glob(f'{subj}/*.mp3')) + list(glob(f'{subj}/*.pth'))
|
||||
return voices
|
||||
|
||||
def get_voice( name, dir=get_voice_dir(), load_latents=True, extensions=["wav", "mp3", "flac"] ):
|
||||
subj = f'{dir}/{name}/'
|
||||
if not os.path.isdir(subj):
|
||||
return
|
||||
files = os.listdir(subj)
|
||||
|
||||
if load_latents:
|
||||
extensions.append("pth")
|
||||
|
||||
voice = []
|
||||
for file in files:
|
||||
ext = os.path.splitext(file)[-1][1:]
|
||||
if ext not in extensions:
|
||||
continue
|
||||
|
||||
voice.append(f'{subj}/{file}')
|
||||
|
||||
return sorted( voice )
|
||||
|
||||
def get_voice_list(dir=get_voice_dir(), append_defaults=False, load_latents=True, extensions=["wav", "mp3", "flac"]):
|
||||
defaults = [ "random", "microphone" ]
|
||||
os.makedirs(dir, exist_ok=True)
|
||||
#res = sorted([d for d in os.listdir(dir) if d not in defaults and os.path.isdir(os.path.join(dir, d)) and len(os.listdir(os.path.join(dir, d))) > 0 ])
|
||||
|
||||
res = []
|
||||
for name in os.listdir(dir):
|
||||
if name in defaults:
|
||||
continue
|
||||
if not os.path.isdir(f'{dir}/{name}'):
|
||||
continue
|
||||
if len(os.listdir(os.path.join(dir, name))) == 0:
|
||||
continue
|
||||
files = get_voice( name, dir=dir, extensions=extensions, load_latents=load_latents )
|
||||
|
||||
if len(files) > 0:
|
||||
res.append(name)
|
||||
else:
|
||||
for subdir in os.listdir(f'{dir}/{name}'):
|
||||
if not os.path.isdir(f'{dir}/{name}/{subdir}'):
|
||||
continue
|
||||
files = get_voice( f'{name}/{subdir}', dir=dir, extensions=extensions, load_latents=load_latents )
|
||||
if len(files) == 0:
|
||||
continue
|
||||
res.append(f'{name}/{subdir}')
|
||||
|
||||
res = sorted(res)
|
||||
|
||||
if append_defaults:
|
||||
res = res + defaults
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _get_voices( dirs=[get_voice_dir()], load_latents=True ):
|
||||
voices = {}
|
||||
for dir in dirs:
|
||||
voice_list = get_voice_list(dir=dir)
|
||||
voices |= { name: get_voice(name=name, dir=dir, load_latents=load_latents) for name in voice_list }
|
||||
|
||||
return voices
|
||||
|
||||
def load_voice(voice, extra_voice_dirs=[], load_latents=True, sample_rate=22050, device='cpu', model_hash=None):
|
||||
def load_voice(voice, extra_voice_dirs=[], load_latents=True, sample_rate=22050, device='cpu'):
|
||||
if voice == 'random':
|
||||
return None, None
|
||||
|
||||
voices = _get_voices(dirs=[get_voice_dir()] + extra_voice_dirs, load_latents=load_latents)
|
||||
|
||||
voices = get_voices(extra_voice_dirs)
|
||||
paths = voices[voice]
|
||||
mtime = 0
|
||||
|
||||
latent = None
|
||||
voices = []
|
||||
|
||||
for path in paths:
|
||||
filename = os.path.basename(path)
|
||||
if filename[-4:] == ".pth" and filename[:12] == "cond_latents":
|
||||
if not model_hash and filename == "cond_latents.pth":
|
||||
latent = path
|
||||
elif model_hash and filename == f"cond_latents_{model_hash[:8]}.pth":
|
||||
latent = path
|
||||
mtime = 0
|
||||
voices = []
|
||||
latent = None
|
||||
for file in paths:
|
||||
if file[-16:] == "cond_latents.pth":
|
||||
latent = file
|
||||
elif file[-4:] == ".pth":
|
||||
{}
|
||||
# noop
|
||||
else:
|
||||
voices.append(path)
|
||||
mtime = max(mtime, os.path.getmtime(path))
|
||||
voices.append(file)
|
||||
mtime = max(mtime, os.path.getmtime(file))
|
||||
|
||||
if load_latents and latent is not None:
|
||||
if os.path.getmtime(latent) > mtime:
|
||||
@ -184,11 +135,11 @@ def load_voice(voice, extra_voice_dirs=[], load_latents=True, sample_rate=22050,
|
||||
return None, torch.load(latent, map_location=device)
|
||||
print(f"Latent file out of date: {latent}")
|
||||
|
||||
samples = []
|
||||
for path in voices:
|
||||
c = load_audio(path, sample_rate)
|
||||
samples.append(c)
|
||||
return samples, None
|
||||
conds = []
|
||||
for cond_path in voices:
|
||||
c = load_audio(cond_path, sample_rate)
|
||||
conds.append(c)
|
||||
return conds, None
|
||||
|
||||
|
||||
def load_voices(voices, extra_voice_dirs=[]):
|
||||
|
||||
@ -1,130 +1,88 @@
|
||||
import torch
|
||||
import psutil
|
||||
import importlib
|
||||
|
||||
DEVICE_OVERRIDE = None
|
||||
DEVICE_BATCH_SIZE_MAP = [(14, 16), (10,8), (7,4)]
|
||||
|
||||
from inspect import currentframe, getframeinfo
|
||||
import gc
|
||||
|
||||
def do_gc():
|
||||
gc.collect()
|
||||
try:
|
||||
torch.cuda.empty_cache()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def print_stats(collect=False):
|
||||
cf = currentframe().f_back
|
||||
msg = f'{getframeinfo(cf).filename}:{cf.f_lineno}'
|
||||
|
||||
if collect:
|
||||
do_gc()
|
||||
|
||||
tot = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
|
||||
res = torch.cuda.memory_reserved(0) / (1024 ** 3)
|
||||
alloc = torch.cuda.memory_allocated(0) / (1024 ** 3)
|
||||
print("[{}] Total: {:.3f} | Reserved: {:.3f} | Allocated: {:.3f} | Free: {:.3f}".format( msg, tot, res, alloc, tot-res ))
|
||||
|
||||
|
||||
def has_dml():
|
||||
loader = importlib.find_loader('torch_directml')
|
||||
if loader is None:
|
||||
return False
|
||||
|
||||
import torch_directml
|
||||
return torch_directml.is_available()
|
||||
|
||||
def set_device_name(name):
|
||||
global DEVICE_OVERRIDE
|
||||
DEVICE_OVERRIDE = name
|
||||
|
||||
def get_device_name(attempt_gc=True):
|
||||
global DEVICE_OVERRIDE
|
||||
if DEVICE_OVERRIDE is not None and DEVICE_OVERRIDE != "":
|
||||
return DEVICE_OVERRIDE
|
||||
|
||||
name = 'cpu'
|
||||
|
||||
if torch.cuda.is_available():
|
||||
name = 'cuda'
|
||||
if attempt_gc:
|
||||
torch.cuda.empty_cache() # may have performance implications
|
||||
elif has_dml():
|
||||
name = 'dml'
|
||||
|
||||
return name
|
||||
|
||||
def get_device(verbose=False):
|
||||
name = get_device_name()
|
||||
|
||||
if verbose:
|
||||
if name == 'cpu':
|
||||
print("No hardware acceleration is available, falling back to CPU...")
|
||||
else:
|
||||
print(f"Hardware acceleration found: {name}")
|
||||
|
||||
if name == "dml":
|
||||
import torch_directml
|
||||
return torch_directml.device()
|
||||
|
||||
return torch.device(name)
|
||||
|
||||
def get_device_vram( name=get_device_name() ):
|
||||
available = 1
|
||||
|
||||
if name == "cuda":
|
||||
_, available = torch.cuda.mem_get_info()
|
||||
elif name == "cpu":
|
||||
available = psutil.virtual_memory()[4]
|
||||
|
||||
return available / (1024 ** 3)
|
||||
|
||||
def get_device_batch_size(name=get_device_name()):
|
||||
vram = get_device_vram(name)
|
||||
|
||||
if vram > 14:
|
||||
return 16
|
||||
elif vram > 10:
|
||||
return 8
|
||||
elif vram > 7:
|
||||
return 4
|
||||
"""
|
||||
for k, v in DEVICE_BATCH_SIZE_MAP:
|
||||
if vram > k:
|
||||
return v
|
||||
"""
|
||||
return 1
|
||||
|
||||
def get_device_count(name=get_device_name()):
|
||||
if name == "cuda":
|
||||
return torch.cuda.device_count()
|
||||
if name == "dml":
|
||||
import torch_directml
|
||||
return torch_directml.device_count()
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
# if you're getting errors make sure you've updated your torch-directml, and if you're still getting errors then you can uncomment the below block
|
||||
"""
|
||||
if has_dml():
|
||||
_cumsum = torch.cumsum
|
||||
_repeat_interleave = torch.repeat_interleave
|
||||
_multinomial = torch.multinomial
|
||||
|
||||
_Tensor_new = torch.Tensor.new
|
||||
_Tensor_cumsum = torch.Tensor.cumsum
|
||||
_Tensor_repeat_interleave = torch.Tensor.repeat_interleave
|
||||
_Tensor_multinomial = torch.Tensor.multinomial
|
||||
|
||||
torch.cumsum = lambda input, *args, **kwargs: ( _cumsum(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
torch.repeat_interleave = lambda input, *args, **kwargs: ( _repeat_interleave(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
torch.multinomial = lambda input, *args, **kwargs: ( _multinomial(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
|
||||
torch.Tensor.new = lambda self, *args, **kwargs: ( _Tensor_new(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.cumsum = lambda self, *args, **kwargs: ( _Tensor_cumsum(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.repeat_interleave = lambda self, *args, **kwargs: ( _Tensor_repeat_interleave(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.multinomial = lambda self, *args, **kwargs: ( _Tensor_multinomial(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
"""
|
||||
import torch
|
||||
import psutil
|
||||
import importlib
|
||||
|
||||
def has_dml():
|
||||
loader = importlib.find_loader('torch_directml')
|
||||
if loader is None:
|
||||
return False
|
||||
|
||||
import torch_directml
|
||||
return torch_directml.is_available()
|
||||
|
||||
def get_device_name():
|
||||
name = 'cpu'
|
||||
|
||||
if has_dml():
|
||||
name = 'dml'
|
||||
elif torch.cuda.is_available():
|
||||
name = 'cuda'
|
||||
|
||||
return name
|
||||
|
||||
def get_device(verbose=False):
|
||||
name = get_device_name()
|
||||
|
||||
if verbose:
|
||||
if name == 'cpu':
|
||||
print("No hardware acceleration is available, falling back to CPU...")
|
||||
else:
|
||||
print(f"Hardware acceleration found: {name}")
|
||||
|
||||
if name == "dml":
|
||||
import torch_directml
|
||||
return torch_directml.device()
|
||||
|
||||
return torch.device(name)
|
||||
|
||||
def get_device_batch_size():
|
||||
available = 1
|
||||
name = get_device_name()
|
||||
|
||||
if name == "dml":
|
||||
# there's nothing publically accessible in the DML API that exposes this
|
||||
# there's a method to get currently used RAM statistics... as tiles
|
||||
available = 1
|
||||
elif name == "cuda":
|
||||
_, available = torch.cuda.mem_get_info()
|
||||
elif name == "cpu":
|
||||
available = psutil.virtual_memory()[4]
|
||||
|
||||
availableGb = available / (1024 ** 3)
|
||||
if availableGb > 14:
|
||||
return 16
|
||||
elif availableGb > 10:
|
||||
return 8
|
||||
elif availableGb > 7:
|
||||
return 4
|
||||
return 1
|
||||
|
||||
def get_device_count():
|
||||
name = get_device_name()
|
||||
if name == "cuda":
|
||||
return torch.cuda.device_count()
|
||||
if name == "dml":
|
||||
import torch_directml
|
||||
return torch_directml.device_count()
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if has_dml():
|
||||
_cumsum = torch.cumsum
|
||||
_repeat_interleave = torch.repeat_interleave
|
||||
_multinomial = torch.multinomial
|
||||
|
||||
_Tensor_new = torch.Tensor.new
|
||||
_Tensor_cumsum = torch.Tensor.cumsum
|
||||
_Tensor_repeat_interleave = torch.Tensor.repeat_interleave
|
||||
_Tensor_multinomial = torch.Tensor.multinomial
|
||||
|
||||
torch.cumsum = lambda input, *args, **kwargs: ( _cumsum(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
torch.repeat_interleave = lambda input, *args, **kwargs: ( _repeat_interleave(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
torch.multinomial = lambda input, *args, **kwargs: ( _multinomial(input.to("cpu"), *args, **kwargs).to(input.device) )
|
||||
|
||||
torch.Tensor.new = lambda self, *args, **kwargs: ( _Tensor_new(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.cumsum = lambda self, *args, **kwargs: ( _Tensor_cumsum(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.repeat_interleave = lambda self, *args, **kwargs: ( _Tensor_repeat_interleave(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
torch.Tensor.multinomial = lambda self, *args, **kwargs: ( _Tensor_multinomial(self.to("cpu"), *args, **kwargs).to(self.device) )
|
||||
@ -13,7 +13,15 @@ import math
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch as th
|
||||
from tqdm.auto import tqdm
|
||||
from tqdm import tqdm
|
||||
|
||||
def tqdm_override(arr, verbose=False, progress=None, desc=None):
|
||||
if verbose and desc is not None:
|
||||
print(desc)
|
||||
|
||||
if progress is None:
|
||||
return tqdm(arr, disable=not verbose)
|
||||
return progress.tqdm(arr, desc=desc, track_tqdm=True)
|
||||
|
||||
def normal_kl(mean1, logvar1, mean2, logvar2):
|
||||
"""
|
||||
@ -548,6 +556,7 @@ class GaussianDiffusion:
|
||||
model_kwargs=None,
|
||||
device=None,
|
||||
verbose=False,
|
||||
progress=None,
|
||||
desc=None
|
||||
):
|
||||
"""
|
||||
@ -580,6 +589,7 @@ class GaussianDiffusion:
|
||||
model_kwargs=model_kwargs,
|
||||
device=device,
|
||||
verbose=verbose,
|
||||
progress=progress,
|
||||
desc=desc
|
||||
):
|
||||
final = sample
|
||||
@ -596,6 +606,7 @@ class GaussianDiffusion:
|
||||
model_kwargs=None,
|
||||
device=None,
|
||||
verbose=False,
|
||||
progress=None,
|
||||
desc=None
|
||||
):
|
||||
"""
|
||||
@ -615,7 +626,7 @@ class GaussianDiffusion:
|
||||
img = th.randn(*shape, device=device)
|
||||
indices = list(range(self.num_timesteps))[::-1]
|
||||
|
||||
for i in tqdm(indices, desc=desc):
|
||||
for i in tqdm_override(indices, verbose=verbose, desc=desc, progress=progress):
|
||||
t = th.tensor([i] * shape[0], device=device)
|
||||
with th.no_grad():
|
||||
out = self.p_sample(
|
||||
@ -730,6 +741,7 @@ class GaussianDiffusion:
|
||||
device=None,
|
||||
verbose=False,
|
||||
eta=0.0,
|
||||
progress=None,
|
||||
desc=None,
|
||||
):
|
||||
"""
|
||||
@ -749,6 +761,7 @@ class GaussianDiffusion:
|
||||
device=device,
|
||||
verbose=verbose,
|
||||
eta=eta,
|
||||
progress=progress,
|
||||
desc=desc
|
||||
):
|
||||
final = sample
|
||||
@ -766,6 +779,7 @@ class GaussianDiffusion:
|
||||
device=None,
|
||||
verbose=False,
|
||||
eta=0.0,
|
||||
progress=None,
|
||||
desc=None,
|
||||
):
|
||||
"""
|
||||
@ -784,7 +798,10 @@ class GaussianDiffusion:
|
||||
indices = list(range(self.num_timesteps))[::-1]
|
||||
|
||||
if verbose:
|
||||
indices = tqdm(indices, desc=desc)
|
||||
# Lazy import so that we don't depend on tqdm.
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
indices = tqdm_override(indices, verbose=verbose, desc=desc, progress=progress)
|
||||
|
||||
for i in indices:
|
||||
t = th.tensor([i] * shape[0], device=device)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
import inflect
|
||||
import torch
|
||||
@ -171,39 +170,16 @@ DEFAULT_VOCAB_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), '
|
||||
|
||||
|
||||
class VoiceBpeTokenizer:
|
||||
def __init__(self, vocab_file=DEFAULT_VOCAB_FILE, preprocess=None):
|
||||
with open(vocab_file, 'r', encoding='utf-8') as f:
|
||||
vocab = json.load(f)
|
||||
|
||||
self.language = vocab['model']['language'] if 'language' in vocab['model'] else None
|
||||
|
||||
if preprocess is None:
|
||||
self.preprocess = 'pre_tokenizer' in vocab and vocab['pre_tokenizer']
|
||||
else:
|
||||
self.preprocess = preprocess
|
||||
def __init__(self, vocab_file=DEFAULT_VOCAB_FILE):
|
||||
if vocab_file is not None:
|
||||
self.tokenizer = Tokenizer.from_file(vocab_file)
|
||||
|
||||
def preprocess_text(self, txt):
|
||||
if self.language == 'ja':
|
||||
import pykakasi
|
||||
|
||||
kks = pykakasi.kakasi()
|
||||
results = kks.convert(txt)
|
||||
words = []
|
||||
|
||||
for result in results:
|
||||
words.append(result['kana'])
|
||||
|
||||
txt = " ".join(words)
|
||||
txt = basic_cleaners(txt)
|
||||
else:
|
||||
txt = english_cleaners(txt)
|
||||
txt = english_cleaners(txt)
|
||||
return txt
|
||||
|
||||
def encode(self, txt):
|
||||
if self.preprocess:
|
||||
txt = self.preprocess_text(txt)
|
||||
txt = self.preprocess_text(txt)
|
||||
txt = txt.replace(' ', '[SPACE]')
|
||||
return self.tokenizer.encode(txt).ids
|
||||
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
"""
|
||||
from bitsandbytes.nn import Linear8bitLt as Linear
|
||||
from bitsandbytes.nn import StableEmbedding as Embedding
|
||||
from bitsandbytes.optim.adam import Adam8bit as Adam
|
||||
from bitsandbytes.optim.adamw import AdamW8bit as AdamW
|
||||
"""
|
||||
"""
|
||||
from torch.nn import Linear
|
||||
from torch.nn import Embedding
|
||||
from torch.optim.adam import Adam
|
||||
from torch.optim.adamw import AdamW
|
||||
"""
|
||||
|
||||
"""
|
||||
OVERRIDE_LINEAR = False
|
||||
OVERRIDE_EMBEDDING = False
|
||||
OVERRIDE_ADAM = False # True
|
||||
OVERRIDE_ADAMW = False # True
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
USE_STABLE_EMBEDDING = False
|
||||
try:
|
||||
OVERRIDE_LINEAR = False
|
||||
OVERRIDE_EMBEDDING = False
|
||||
OVERRIDE_ADAM = False
|
||||
OVERRIDE_ADAMW = False
|
||||
|
||||
USE_STABLE_EMBEDDING = os.environ.get('BITSANDBYTES_USE_STABLE_EMBEDDING', '1' if USE_STABLE_EMBEDDING else '0') == '1'
|
||||
OVERRIDE_LINEAR = os.environ.get('BITSANDBYTES_OVERRIDE_LINEAR', '1' if OVERRIDE_LINEAR else '0') == '1'
|
||||
OVERRIDE_EMBEDDING = os.environ.get('BITSANDBYTES_OVERRIDE_EMBEDDING', '1' if OVERRIDE_EMBEDDING else '0') == '1'
|
||||
OVERRIDE_ADAM = os.environ.get('BITSANDBYTES_OVERRIDE_ADAM', '1' if OVERRIDE_ADAM else '0') == '1'
|
||||
OVERRIDE_ADAMW = os.environ.get('BITSANDBYTES_OVERRIDE_ADAMW', '1' if OVERRIDE_ADAMW else '0') == '1'
|
||||
|
||||
if OVERRIDE_LINEAR or OVERRIDE_EMBEDDING or OVERRIDE_ADAM or OVERRIDE_ADAMW:
|
||||
import bitsandbytes as bnb
|
||||
except Exception as e:
|
||||
OVERRIDE_LINEAR = False
|
||||
OVERRIDE_EMBEDDING = False
|
||||
OVERRIDE_ADAM = False
|
||||
OVERRIDE_ADAMW = False
|
||||
|
||||
if OVERRIDE_LINEAR:
|
||||
from bitsandbytes.nn import Linear8bitLt as Linear
|
||||
else:
|
||||
from torch.nn import Linear
|
||||
|
||||
if OVERRIDE_EMBEDDING:
|
||||
if USE_STABLE_EMBEDDING:
|
||||
from bitsandbytes.nn import StableEmbedding as Embedding
|
||||
else:
|
||||
from bitsandbytes.nn.modules import Embedding as Embedding
|
||||
else:
|
||||
from torch.nn import Embedding
|
||||
|
||||
if OVERRIDE_ADAM:
|
||||
from bitsandbytes.optim.adam import Adam8bit as Adam
|
||||
else:
|
||||
from torch.optim.adam import Adam
|
||||
|
||||
if OVERRIDE_ADAMW:
|
||||
from bitsandbytes.optim.adamw import AdamW8bit as AdamW
|
||||
else:
|
||||
from torch.optim.adamw import AdamW
|
||||
@ -7,8 +7,6 @@ from transformers import Wav2Vec2ForCTC, Wav2Vec2FeatureExtractor, Wav2Vec2CTCTo
|
||||
from tortoise.utils.audio import load_audio
|
||||
from tortoise.utils.device import get_device
|
||||
|
||||
import tortoise.utils.torch_intermediary as ml
|
||||
|
||||
def max_alignment(s1, s2, skip_character='~', record=None):
|
||||
"""
|
||||
A clever function that aligns s1 to s2 as best it can. Wherever a character from s1 is not found in s2, a '~' is
|
||||
@ -144,7 +142,7 @@ class Wav2VecAlignment:
|
||||
non_redacted_intervals = []
|
||||
last_point = 0
|
||||
for i in range(len(fully_split)):
|
||||
if i % 2 == 0 and fully_split[i] != "": # Check for empty string fixes index error
|
||||
if i % 2 == 0:
|
||||
end_interval = max(0, last_point + len(fully_split[i]) - 1)
|
||||
non_redacted_intervals.append((last_point, end_interval))
|
||||
last_point += len(fully_split[i])
|
||||
|
||||
124
tortoise_tts.ipynb
Executable file
124
tortoise_tts.ipynb
Executable file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"nbformat":4,
|
||||
"nbformat_minor":0,
|
||||
"metadata":{
|
||||
"colab":{
|
||||
"private_outputs":true,
|
||||
"provenance":[
|
||||
|
||||
]
|
||||
},
|
||||
"kernelspec":{
|
||||
"name":"python3",
|
||||
"display_name":"Python 3"
|
||||
},
|
||||
"language_info":{
|
||||
"name":"python"
|
||||
},
|
||||
"accelerator":"GPU",
|
||||
"gpuClass":"standard"
|
||||
},
|
||||
"cells":[
|
||||
{
|
||||
"cell_type":"markdown",
|
||||
"source":[
|
||||
"## Initialization"
|
||||
],
|
||||
"metadata":{
|
||||
"id":"ni41hmE03DL6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type":"code",
|
||||
"execution_count":null,
|
||||
"metadata":{
|
||||
"id":"FtsMKKfH18iM"
|
||||
},
|
||||
"outputs":[
|
||||
|
||||
],
|
||||
"source":[
|
||||
"!git clone https://git.ecker.tech/mrq/tortoise-tts/\n",
|
||||
"%cd tortoise-tts\n",
|
||||
"!python -m pip install --upgrade pip\n",
|
||||
"!pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu116\n",
|
||||
"!python -m pip install -r ./requirements.txt\n",
|
||||
"!python setup.py install"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type":"code",
|
||||
"source":[
|
||||
"# colab requires the runtime to restart before use\n",
|
||||
"exit()"
|
||||
],
|
||||
"metadata":{
|
||||
"id":"FVUOtSASCSJ8"
|
||||
},
|
||||
"execution_count":null,
|
||||
"outputs":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type":"markdown",
|
||||
"source":[
|
||||
"## Running"
|
||||
],
|
||||
"metadata":{
|
||||
"id":"o1gkfw3B3JSk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type":"code",
|
||||
"source":[
|
||||
"%cd tortoise-tts\n",
|
||||
"import webui as mrq\n",
|
||||
"import sys\n",
|
||||
"sys.argv = [\"\"]\n",
|
||||
"\n",
|
||||
"mrq.args = mrq.setup_args()\n",
|
||||
"mrq.webui = mrq.setup_gradio()\n",
|
||||
"mrq.webui.launch(share=True, prevent_thread_lock=True, height=1000)\n",
|
||||
"mrq.tts = mrq.setup_tortoise()\n",
|
||||
"mrq.webui.block_thread()"
|
||||
],
|
||||
"metadata":{
|
||||
"id":"c_EQZLTA19c7"
|
||||
},
|
||||
"execution_count":null,
|
||||
"outputs":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type":"markdown",
|
||||
"source":[
|
||||
"## Exporting"
|
||||
],
|
||||
"metadata":{
|
||||
"id":"2AnVQxEJx47p"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type":"code",
|
||||
"source":[
|
||||
"!apt install -y p7zip-full\n",
|
||||
"from datetime import datetime\n",
|
||||
"timestamp = datetime.now().strftime('%m-%d-%Y_%H:%M:%S')\n",
|
||||
"!mkdir -p \"../{timestamp}\"\n",
|
||||
"!mv ./results/* \"../{timestamp}/.\"\n",
|
||||
"!7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on \"../{timestamp}.7z\" \"../{timestamp}/\"\n",
|
||||
"!ls ~/\n",
|
||||
"!echo \"Finished zipping, archive is available at {timestamp}.7z\""
|
||||
],
|
||||
"metadata":{
|
||||
"id":"YOACiDCXx72G"
|
||||
},
|
||||
"execution_count":null,
|
||||
"outputs":[
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
3
update-force.bat
Executable file
3
update-force.bat
Executable file
@ -0,0 +1,3 @@
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
call .\update.bat
|
||||
3
update-force.sh
Executable file
3
update-force.sh
Executable file
@ -0,0 +1,3 @@
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
./update.sh
|
||||
7
update.bat
Executable file
7
update.bat
Executable file
@ -0,0 +1,7 @@
|
||||
git pull
|
||||
python -m venv tortoise-venv
|
||||
call .\tortoise-venv\Scripts\activate.bat
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r ./requirements.txt
|
||||
deactivate
|
||||
pause
|
||||
6
update.sh
Executable file
6
update.sh
Executable file
@ -0,0 +1,6 @@
|
||||
git pull
|
||||
python -m venv tortoise-venv
|
||||
source ./tortoise-venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r ./requirements.txt
|
||||
deactivate
|
||||
0
voices/.gitkeep
Executable file
0
voices/.gitkeep
Executable file
813
webui.py
Executable file
813
webui.py
Executable file
@ -0,0 +1,813 @@
|
||||
import os
|
||||
import argparse
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import re
|
||||
import urllib.request
|
||||
|
||||
import torch
|
||||
import torchaudio
|
||||
import music_tag
|
||||
import gradio as gr
|
||||
import gradio.utils
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
import tortoise.api
|
||||
|
||||
from tortoise.api import TextToSpeech
|
||||
from tortoise.utils.audio import load_audio, load_voice, load_voices, get_voice_dir
|
||||
from tortoise.utils.text import split_and_recombine_text
|
||||
|
||||
args = None
|
||||
webui = None
|
||||
tts = None
|
||||
|
||||
def generate(
|
||||
text,
|
||||
delimiter,
|
||||
emotion,
|
||||
prompt,
|
||||
voice,
|
||||
mic_audio,
|
||||
seed,
|
||||
candidates,
|
||||
num_autoregressive_samples,
|
||||
diffusion_iterations,
|
||||
temperature,
|
||||
diffusion_sampler,
|
||||
breathing_room,
|
||||
cvvp_weight,
|
||||
top_p,
|
||||
diffusion_temperature,
|
||||
length_penalty,
|
||||
repetition_penalty,
|
||||
cond_free_k,
|
||||
experimental_checkboxes,
|
||||
progress=gr.Progress(track_tqdm=True)
|
||||
):
|
||||
try:
|
||||
tts
|
||||
except NameError:
|
||||
raise gr.Error("TTS is still initializing...")
|
||||
|
||||
|
||||
if voice != "microphone":
|
||||
voices = [voice]
|
||||
else:
|
||||
voices = []
|
||||
|
||||
if voice == "microphone":
|
||||
if mic_audio is None:
|
||||
raise gr.Error("Please provide audio from mic when choosing `microphone` as a voice input")
|
||||
mic = load_audio(mic_audio, tts.input_sample_rate)
|
||||
voice_samples, conditioning_latents = [mic], None
|
||||
else:
|
||||
progress(0, desc="Loading voice...")
|
||||
voice_samples, conditioning_latents = load_voice(voice)
|
||||
|
||||
if voice_samples is not None:
|
||||
sample_voice = voice_samples[0].squeeze().cpu()
|
||||
|
||||
conditioning_latents = tts.get_conditioning_latents(voice_samples, return_mels=not args.latents_lean_and_mean, progress=progress, max_chunk_size=args.cond_latent_max_chunk_size)
|
||||
if len(conditioning_latents) == 4:
|
||||
conditioning_latents = (conditioning_latents[0], conditioning_latents[1], conditioning_latents[2], None)
|
||||
|
||||
if voice != "microphone":
|
||||
torch.save(conditioning_latents, f'./{get_voice_dir()}/{voice}/cond_latents.pth')
|
||||
voice_samples = None
|
||||
else:
|
||||
sample_voice = None
|
||||
|
||||
if seed == 0:
|
||||
seed = None
|
||||
|
||||
if conditioning_latents is not None and len(conditioning_latents) == 2 and cvvp_weight > 0:
|
||||
print("Requesting weighing against CVVP weight, but voice latents are missing some extra data. Please regenerate your voice latents.")
|
||||
cvvp_weight = 0
|
||||
|
||||
|
||||
settings = {
|
||||
'temperature': float(temperature),
|
||||
|
||||
'top_p': float(top_p),
|
||||
'diffusion_temperature': float(diffusion_temperature),
|
||||
'length_penalty': float(length_penalty),
|
||||
'repetition_penalty': float(repetition_penalty),
|
||||
'cond_free_k': float(cond_free_k),
|
||||
|
||||
'num_autoregressive_samples': num_autoregressive_samples,
|
||||
'sample_batch_size': args.sample_batch_size,
|
||||
'diffusion_iterations': diffusion_iterations,
|
||||
|
||||
'voice_samples': voice_samples,
|
||||
'conditioning_latents': conditioning_latents,
|
||||
'use_deterministic_seed': seed,
|
||||
'return_deterministic_state': True,
|
||||
'k': candidates,
|
||||
'diffusion_sampler': diffusion_sampler,
|
||||
'breathing_room': breathing_room,
|
||||
'progress': progress,
|
||||
'half_p': "Half Precision" in experimental_checkboxes,
|
||||
'cond_free': "Conditioning-Free" in experimental_checkboxes,
|
||||
'cvvp_amount': cvvp_weight,
|
||||
}
|
||||
|
||||
if delimiter == "\\n":
|
||||
delimiter = "\n"
|
||||
|
||||
if delimiter != "" and delimiter in text:
|
||||
texts = text.split(delimiter)
|
||||
else:
|
||||
texts = split_and_recombine_text(text)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
outdir = f"./results/{voice}/"
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
audio_cache = {}
|
||||
|
||||
resampler = torchaudio.transforms.Resample(
|
||||
tts.output_sample_rate,
|
||||
args.output_sample_rate,
|
||||
lowpass_filter_width=16,
|
||||
rolloff=0.85,
|
||||
resampling_method="kaiser_window",
|
||||
beta=8.555504641634386,
|
||||
) if tts.output_sample_rate != args.output_sample_rate else None
|
||||
|
||||
volume_adjust = torchaudio.transforms.Vol(gain=args.output_volume, gain_type="amplitude") if args.output_volume != 1 else None
|
||||
|
||||
idx = 0
|
||||
for i, file in enumerate(os.listdir(outdir)):
|
||||
if file[-5:] == ".json":
|
||||
idx = idx + 1
|
||||
|
||||
def get_name(line=0, candidate=0, combined=False):
|
||||
if combined:
|
||||
return f"{idx}_combined"
|
||||
|
||||
name = f"{idx}"
|
||||
if len(texts) > 1:
|
||||
name = f"{name}_{line}"
|
||||
if candidates > 1:
|
||||
name = f"{name}_{candidate}"
|
||||
return name
|
||||
|
||||
for line, cut_text in enumerate(texts):
|
||||
if emotion == "Custom":
|
||||
if prompt.strip() != "":
|
||||
cut_text = f"[{prompt},] {cut_text}"
|
||||
else:
|
||||
cut_text = f"[I am really {emotion.lower()},] {cut_text}"
|
||||
|
||||
print(f"[{str(line+1)}/{str(len(texts))}] Generating line: {cut_text}")
|
||||
|
||||
gen, additionals = tts.tts(cut_text, **settings )
|
||||
seed = additionals[0]
|
||||
|
||||
if isinstance(gen, list):
|
||||
for j, g in enumerate(gen):
|
||||
name = get_name(line=line, candidate=j)
|
||||
audio_cache[name] = {
|
||||
'audio': g,
|
||||
'text': cut_text,
|
||||
}
|
||||
else:
|
||||
name = get_name(line=line)
|
||||
audio_cache[name] = {
|
||||
'audio': gen,
|
||||
'text': cut_text,
|
||||
}
|
||||
|
||||
for k in audio_cache:
|
||||
audio = audio_cache[k]['audio'].squeeze(0).cpu()
|
||||
if resampler is not None:
|
||||
audio = resampler(audio)
|
||||
if volume_adjust is not None:
|
||||
audio = volume_adjust(audio)
|
||||
|
||||
audio_cache[k]['audio'] = audio
|
||||
torchaudio.save(f'{outdir}/{voice}_{k}.wav', audio, args.output_sample_rate)
|
||||
|
||||
output_voice = None
|
||||
output_voices = []
|
||||
if len(texts) > 1:
|
||||
for candidate in range(candidates):
|
||||
audio_clips = []
|
||||
for line in range(len(texts)):
|
||||
if isinstance(gen, list):
|
||||
name = get_name(line=line, candidate=candidate)
|
||||
audio = audio_cache[name]['audio']
|
||||
else:
|
||||
name = get_name(line=line)
|
||||
audio = audio_cache[name]['audio']
|
||||
audio_clips.append(audio)
|
||||
|
||||
name = get_name(combined=True)
|
||||
audio = torch.cat(audio_clips, dim=-1)
|
||||
torchaudio.save(f'{outdir}/{voice}_{name}.wav', audio, args.output_sample_rate)
|
||||
|
||||
audio = audio.squeeze(0).cpu()
|
||||
audio_cache[name] = {
|
||||
'audio': audio,
|
||||
'text': text,
|
||||
}
|
||||
|
||||
output_voices.append(f'{outdir}/{voice}_{name}.wav')
|
||||
if output_voice is None:
|
||||
output_voice = f'{outdir}/{voice}_{name}.wav'
|
||||
# output_voice = audio
|
||||
else:
|
||||
if candidates > 1:
|
||||
for candidate in range(candidates):
|
||||
name = get_name(candidate=candidate)
|
||||
output_voices.append(f'{outdir}/{voice}_{name}.wav')
|
||||
else:
|
||||
name = get_name()
|
||||
output_voices.append(f'{outdir}/{voice}_{name}.wav')
|
||||
#output_voice = f'{outdir}/{voice}_{name}.wav'
|
||||
|
||||
info = {
|
||||
'text': text,
|
||||
'delimiter': '\\n' if delimiter == "\n" else delimiter,
|
||||
'emotion': emotion,
|
||||
'prompt': prompt,
|
||||
'voice': voice,
|
||||
'mic_audio': mic_audio,
|
||||
'seed': seed,
|
||||
'candidates': candidates,
|
||||
'num_autoregressive_samples': num_autoregressive_samples,
|
||||
'diffusion_iterations': diffusion_iterations,
|
||||
'temperature': temperature,
|
||||
'diffusion_sampler': diffusion_sampler,
|
||||
'breathing_room': breathing_room,
|
||||
'cvvp_weight': cvvp_weight,
|
||||
'top_p': top_p,
|
||||
'diffusion_temperature': diffusion_temperature,
|
||||
'length_penalty': length_penalty,
|
||||
'repetition_penalty': repetition_penalty,
|
||||
'cond_free_k': cond_free_k,
|
||||
'experimentals': experimental_checkboxes,
|
||||
'time': time.time()-start_time,
|
||||
}
|
||||
|
||||
with open(f'{outdir}/input_{idx}.json', 'w', encoding="utf-8") as f:
|
||||
f.write(json.dumps(info, indent='\t') )
|
||||
|
||||
if voice is not None and conditioning_latents is not None:
|
||||
with open(f'./{get_voice_dir()}/{voice}/cond_latents.pth', 'rb') as f:
|
||||
info['latents'] = base64.b64encode(f.read()).decode("ascii")
|
||||
|
||||
if args.embed_output_metadata:
|
||||
for path in audio_cache:
|
||||
info['text'] = audio_cache[path]['text']
|
||||
|
||||
metadata = music_tag.load_file(f"{outdir}/{voice}_{path}.wav")
|
||||
metadata['lyrics'] = json.dumps(info)
|
||||
metadata.save()
|
||||
|
||||
#if output_voice is not None:
|
||||
# output_voice = (args.output_sample_rate, output_voice.numpy())
|
||||
|
||||
if sample_voice is not None:
|
||||
sample_voice = (tts.input_sample_rate, sample_voice.numpy())
|
||||
|
||||
print(f"Generation took {info['time']} seconds, saved to '{outdir}'\n")
|
||||
|
||||
info['seed'] = settings['use_deterministic_seed']
|
||||
del info['latents']
|
||||
with open(f'./config/generate.json', 'w', encoding="utf-8") as f:
|
||||
f.write(json.dumps(info, indent='\t') )
|
||||
|
||||
results = [
|
||||
[ seed, "{:.3f}".format(info['time']) ]
|
||||
]
|
||||
|
||||
return (
|
||||
sample_voice,
|
||||
output_voice if output_voice is not None else output_voices[0],
|
||||
results,
|
||||
)
|
||||
|
||||
def update_presets(value):
|
||||
PRESETS = {
|
||||
'Ultra Fast': {'num_autoregressive_samples': 16, 'diffusion_iterations': 30, 'cond_free': False},
|
||||
'Fast': {'num_autoregressive_samples': 96, 'diffusion_iterations': 80},
|
||||
'Standard': {'num_autoregressive_samples': 256, 'diffusion_iterations': 200},
|
||||
'High Quality': {'num_autoregressive_samples': 256, 'diffusion_iterations': 400},
|
||||
}
|
||||
|
||||
if value in PRESETS:
|
||||
preset = PRESETS[value]
|
||||
return (gr.update(value=preset['num_autoregressive_samples']), gr.update(value=preset['diffusion_iterations']))
|
||||
else:
|
||||
return (gr.update(), gr.update())
|
||||
|
||||
def read_generate_settings(file, save_latents=True, save_as_temp=True):
|
||||
j = None
|
||||
latents = None
|
||||
|
||||
if file is not None:
|
||||
if hasattr(file, 'name'):
|
||||
file = file.name
|
||||
|
||||
if file[-4:] == ".wav":
|
||||
metadata = music_tag.load_file(file)
|
||||
if 'lyrics' in metadata:
|
||||
j = json.loads(str(metadata['lyrics']))
|
||||
elif file[-5:] == ".json":
|
||||
with open(file, 'r') as f:
|
||||
j = json.load(f)
|
||||
|
||||
if 'latents' in j and save_latents:
|
||||
latents = base64.b64decode(j['latents'])
|
||||
del j['latents']
|
||||
|
||||
if latents and save_latents:
|
||||
outdir=f'./{get_voice_dir()}/{".temp" if save_as_temp else j["voice"]}/'
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
with open(f'{outdir}/cond_latents.pth', 'wb') as f:
|
||||
f.write(latents)
|
||||
latents = f'{outdir}/cond_latents.pth'
|
||||
|
||||
if "time" in j:
|
||||
j["time"] = "{:.3f}".format(j["time"])
|
||||
|
||||
return (
|
||||
j,
|
||||
latents
|
||||
)
|
||||
def save_latents(file):
|
||||
read_generate_settings(file, save_latents=True, save_as_temp=False)
|
||||
|
||||
def import_generate_settings(file="./config/generate.json"):
|
||||
settings, _ = read_generate_settings(file, save_latents=False)
|
||||
|
||||
if settings is None:
|
||||
return None
|
||||
|
||||
return (
|
||||
None if 'text' not in settings else settings['text'],
|
||||
None if 'delimiter' not in settings else settings['delimiter'],
|
||||
None if 'emotion' not in settings else settings['emotion'],
|
||||
None if 'prompt' not in settings else settings['prompt'],
|
||||
None if 'voice' not in settings else settings['voice'],
|
||||
None if 'mic_audio' not in settings else settings['mic_audio'],
|
||||
None if 'seed' not in settings else settings['seed'],
|
||||
None if 'candidates' not in settings else settings['candidates'],
|
||||
None if 'num_autoregressive_samples' not in settings else settings['num_autoregressive_samples'],
|
||||
None if 'diffusion_iterations' not in settings else settings['diffusion_iterations'],
|
||||
0.8 if 'temperature' not in settings else settings['temperature'],
|
||||
"DDIM" if 'diffusion_sampler' not in settings else settings['diffusion_sampler'],
|
||||
8 if 'breathing_room' not in settings else settings['breathing_room'],
|
||||
0.0 if 'cvvp_weight' not in settings else settings['cvvp_weight'],
|
||||
0.8 if 'top_p' not in settings else settings['top_p'],
|
||||
1.0 if 'diffusion_temperature' not in settings else settings['diffusion_temperature'],
|
||||
1.0 if 'length_penalty' not in settings else settings['length_penalty'],
|
||||
2.0 if 'repetition_penalty' not in settings else settings['repetition_penalty'],
|
||||
2.0 if 'cond_free_k' not in settings else settings['cond_free_k'],
|
||||
None if 'experimentals' not in settings else settings['experimentals'],
|
||||
)
|
||||
|
||||
def curl(url):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Python'})
|
||||
conn = urllib.request.urlopen(req)
|
||||
data = conn.read()
|
||||
data = data.decode()
|
||||
data = json.loads(data)
|
||||
conn.close()
|
||||
return data
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def check_for_updates():
|
||||
if not os.path.isfile('./.git/FETCH_HEAD'):
|
||||
print("Cannot check for updates: not from a git repo")
|
||||
return False
|
||||
|
||||
with open(f'./.git/FETCH_HEAD', 'r', encoding="utf-8") as f:
|
||||
head = f.read()
|
||||
|
||||
match = re.findall(r"^([a-f0-9]+).+?https:\/\/(.+?)\/(.+?)\/(.+?)\n", head)
|
||||
if match is None or len(match) == 0:
|
||||
print("Cannot check for updates: cannot parse FETCH_HEAD")
|
||||
return False
|
||||
|
||||
match = match[0]
|
||||
|
||||
local = match[0]
|
||||
host = match[1]
|
||||
owner = match[2]
|
||||
repo = match[3]
|
||||
|
||||
res = curl(f"https://{host}/api/v1/repos/{owner}/{repo}/branches/") #this only works for gitea instances
|
||||
|
||||
if res is None or len(res) == 0:
|
||||
print("Cannot check for updates: cannot fetch from remote")
|
||||
return False
|
||||
|
||||
remote = res[0]["commit"]["id"]
|
||||
|
||||
if remote != local:
|
||||
print(f"New version found: {local[:8]} => {remote[:8]}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def reload_tts():
|
||||
tts = setup_tortoise()
|
||||
|
||||
def cancel_generate():
|
||||
tortoise.api.STOP_SIGNAL = True
|
||||
|
||||
def update_voices():
|
||||
return gr.Dropdown.update(choices=sorted(os.listdir(get_voice_dir())) + ["microphone"])
|
||||
|
||||
def export_exec_settings( share, listen, check_for_updates, models_from_local_only, low_vram, embed_output_metadata, latents_lean_and_mean, cond_latent_max_chunk_size, sample_batch_size, concurrency_count, output_sample_rate, output_volume ):
|
||||
args.share = share
|
||||
args.listen = listen
|
||||
args.low_vram = low_vram
|
||||
args.check_for_updates = check_for_updates
|
||||
args.models_from_local_only = models_from_local_only
|
||||
args.cond_latent_max_chunk_size = cond_latent_max_chunk_size
|
||||
args.sample_batch_size = sample_batch_size
|
||||
args.embed_output_metadata = embed_output_metadata
|
||||
args.latents_lean_and_mean = latents_lean_and_mean
|
||||
args.concurrency_count = concurrency_count
|
||||
args.output_sample_rate = output_sample_rate
|
||||
args.output_volume = output_volume
|
||||
|
||||
settings = {
|
||||
'share': args.share,
|
||||
'listen': args.listen,
|
||||
'low-vram':args.low_vram,
|
||||
'check-for-updates':args.check_for_updates,
|
||||
'models-from-local-only':args.models_from_local_only,
|
||||
'cond-latent-max-chunk-size': args.cond_latent_max_chunk_size,
|
||||
'sample-batch-size': args.sample_batch_size,
|
||||
'embed-output-metadata': args.embed_output_metadata,
|
||||
'latents-lean-and-mean': args.latents_lean_and_mean,
|
||||
'concurrency-count': args.concurrency_count,
|
||||
'output-sample-rate': args.output_sample_rate,
|
||||
'output-volume': args.output_volume,
|
||||
}
|
||||
|
||||
with open(f'./config/exec.json', 'w', encoding="utf-8") as f:
|
||||
f.write(json.dumps(settings, indent='\t') )
|
||||
|
||||
def setup_args():
|
||||
default_arguments = {
|
||||
'share': False,
|
||||
'listen': None,
|
||||
'check-for-updates': False,
|
||||
'models-from-local-only': False,
|
||||
'low-vram': False,
|
||||
'sample-batch-size': None,
|
||||
'embed-output-metadata': True,
|
||||
'latents-lean-and-mean': True,
|
||||
'cond-latent-max-chunk-size': 1000000,
|
||||
'concurrency-count': 2,
|
||||
'output-sample-rate': 44100,
|
||||
'output-volume': 1,
|
||||
}
|
||||
|
||||
if os.path.isfile('./config/exec.json'):
|
||||
with open(f'./config/exec.json', 'r', encoding="utf-8") as f:
|
||||
overrides = json.load(f)
|
||||
for k in overrides:
|
||||
default_arguments[k] = overrides[k]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--share", action='store_true', default=default_arguments['share'], help="Lets Gradio return a public URL to use anywhere")
|
||||
parser.add_argument("--listen", default=default_arguments['listen'], help="Path for Gradio to listen on")
|
||||
parser.add_argument("--check-for-updates", action='store_true', default=default_arguments['check-for-updates'], help="Checks for update on startup")
|
||||
parser.add_argument("--models-from-local-only", action='store_true', default=default_arguments['models-from-local-only'], help="Only loads models from disk, does not check for updates for models")
|
||||
parser.add_argument("--low-vram", action='store_true', default=default_arguments['low-vram'], help="Disables some optimizations that increases VRAM usage")
|
||||
parser.add_argument("--no-embed-output-metadata", action='store_false', default=not default_arguments['embed-output-metadata'], help="Disables embedding output metadata into resulting WAV files for easily fetching its settings used with the web UI (data is stored in the lyrics metadata tag)")
|
||||
parser.add_argument("--latents-lean-and-mean", action='store_true', default=default_arguments['latents-lean-and-mean'], help="Exports the bare essentials for latents.")
|
||||
parser.add_argument("--cond-latent-max-chunk-size", default=default_arguments['cond-latent-max-chunk-size'], type=int, help="Sets an upper limit to audio chunk size when computing conditioning latents")
|
||||
parser.add_argument("--sample-batch-size", default=default_arguments['sample-batch-size'], type=int, help="Sets an upper limit to audio chunk size when computing conditioning latents")
|
||||
parser.add_argument("--concurrency-count", type=int, default=default_arguments['concurrency-count'], help="How many Gradio events to process at once")
|
||||
parser.add_argument("--output-sample-rate", type=int, default=default_arguments['output-sample-rate'], help="Sample rate to resample the output to (from 24KHz)")
|
||||
parser.add_argument("--output-volume", type=float, default=default_arguments['output-volume'], help="Adjusts volume of output")
|
||||
args = parser.parse_args()
|
||||
|
||||
args.embed_output_metadata = not args.no_embed_output_metadata
|
||||
|
||||
args.listen_host = None
|
||||
args.listen_port = None
|
||||
args.listen_path = None
|
||||
if args.listen:
|
||||
match = re.findall(r"^(?:(.+?):(\d+))?(\/.+?)?$", args.listen)[0]
|
||||
|
||||
args.listen_host = match[0] if match[0] != "" else "127.0.0.1"
|
||||
args.listen_port = match[1] if match[1] != "" else None
|
||||
args.listen_path = match[2] if match[2] != "" else "/"
|
||||
|
||||
if args.listen_port is not None:
|
||||
args.listen_port = int(args.listen_port)
|
||||
|
||||
return args
|
||||
|
||||
def setup_tortoise():
|
||||
print("Initializating TorToiSe...")
|
||||
tts = TextToSpeech(minor_optimizations=not args.low_vram)
|
||||
print("TorToiSe initialized, ready for generation.")
|
||||
return tts
|
||||
|
||||
def setup_gradio():
|
||||
if not args.share:
|
||||
def noop(function, return_value=None):
|
||||
def wrapped(*args, **kwargs):
|
||||
return return_value
|
||||
return wrapped
|
||||
gradio.utils.version_check = noop(gradio.utils.version_check)
|
||||
gradio.utils.initiated_analytics = noop(gradio.utils.initiated_analytics)
|
||||
gradio.utils.launch_analytics = noop(gradio.utils.launch_analytics)
|
||||
gradio.utils.integration_analytics = noop(gradio.utils.integration_analytics)
|
||||
gradio.utils.error_analytics = noop(gradio.utils.error_analytics)
|
||||
gradio.utils.log_feature_analytics = noop(gradio.utils.log_feature_analytics)
|
||||
#gradio.utils.get_local_ip_address = noop(gradio.utils.get_local_ip_address, 'localhost')
|
||||
|
||||
if args.models_from_local_only:
|
||||
os.environ['TRANSFORMERS_OFFLINE']='1'
|
||||
|
||||
with gr.Blocks() as webui:
|
||||
with gr.Tab("Generate"):
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
text = gr.Textbox(lines=4, label="Prompt")
|
||||
delimiter = gr.Textbox(lines=1, label="Line Delimiter", placeholder="\\n")
|
||||
|
||||
emotion = gr.Radio(
|
||||
["Happy", "Sad", "Angry", "Disgusted", "Arrogant", "Custom"],
|
||||
value="Custom",
|
||||
label="Emotion",
|
||||
type="value",
|
||||
interactive=True
|
||||
)
|
||||
prompt = gr.Textbox(lines=1, label="Custom Emotion + Prompt (if selected)")
|
||||
voice = gr.Dropdown(
|
||||
sorted(os.listdir(get_voice_dir())) + ["microphone"],
|
||||
label="Voice",
|
||||
type="value",
|
||||
)
|
||||
mic_audio = gr.Audio(
|
||||
label="Microphone Source",
|
||||
source="microphone",
|
||||
type="filepath",
|
||||
)
|
||||
refresh_voices = gr.Button(value="Refresh Voice List")
|
||||
refresh_voices.click(update_voices,
|
||||
inputs=None,
|
||||
outputs=voice
|
||||
)
|
||||
|
||||
prompt.change(fn=lambda value: gr.update(value="Custom"),
|
||||
inputs=prompt,
|
||||
outputs=emotion
|
||||
)
|
||||
mic_audio.change(fn=lambda value: gr.update(value="microphone"),
|
||||
inputs=mic_audio,
|
||||
outputs=voice
|
||||
)
|
||||
with gr.Column():
|
||||
candidates = gr.Slider(value=1, minimum=1, maximum=6, step=1, label="Candidates")
|
||||
seed = gr.Number(value=0, precision=0, label="Seed")
|
||||
|
||||
preset = gr.Radio(
|
||||
["Ultra Fast", "Fast", "Standard", "High Quality"],
|
||||
label="Preset",
|
||||
type="value",
|
||||
)
|
||||
num_autoregressive_samples = gr.Slider(value=128, minimum=0, maximum=512, step=1, label="Samples")
|
||||
diffusion_iterations = gr.Slider(value=128, minimum=0, maximum=512, step=1, label="Iterations")
|
||||
|
||||
temperature = gr.Slider(value=0.2, minimum=0, maximum=1, step=0.1, label="Temperature")
|
||||
breathing_room = gr.Slider(value=8, minimum=1, maximum=32, step=1, label="Pause Size")
|
||||
diffusion_sampler = gr.Radio(
|
||||
["P", "DDIM"], # + ["K_Euler_A", "DPM++2M"],
|
||||
value="P",
|
||||
label="Diffusion Samplers",
|
||||
type="value",
|
||||
)
|
||||
|
||||
preset.change(fn=update_presets,
|
||||
inputs=preset,
|
||||
outputs=[
|
||||
num_autoregressive_samples,
|
||||
diffusion_iterations,
|
||||
],
|
||||
)
|
||||
with gr.Column():
|
||||
selected_voice = gr.Audio(label="Source Sample")
|
||||
output_audio = gr.Audio(label="Output")
|
||||
generation_results = gr.Dataframe(label="Results", headers=["Seed", "Time"])
|
||||
|
||||
submit = gr.Button(value="Generate")
|
||||
stop = gr.Button(value="Stop")
|
||||
with gr.Tab("History"):
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
headers = {
|
||||
"Name": "",
|
||||
"Samples": "num_autoregressive_samples",
|
||||
"Iterations": "diffusion_iterations",
|
||||
"Temp.": "temperature",
|
||||
"Sampler": "diffusion_sampler",
|
||||
"CVVP": "cvvp_weight",
|
||||
"Top P": "top_p",
|
||||
"Diff. Temp.": "diffusion_temperature",
|
||||
"Len Pen": "length_penalty",
|
||||
"Rep Pen": "repetition_penalty",
|
||||
"Cond-Free K": "cond_free_k",
|
||||
"Time": "time",
|
||||
}
|
||||
history_info = gr.Dataframe(label="Results", headers=list(headers.keys()))
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
history_voices = gr.Dropdown(
|
||||
sorted(os.listdir(get_voice_dir())) + ["microphone"],
|
||||
label="Voice",
|
||||
type="value",
|
||||
)
|
||||
|
||||
history_view_results_button = gr.Button(value="View Files")
|
||||
with gr.Column():
|
||||
history_results_list = gr.Dropdown(label="Results",type="value", interactive=True)
|
||||
history_view_result_button = gr.Button(value="View File")
|
||||
with gr.Column():
|
||||
history_audio = gr.Audio()
|
||||
history_copy_settings_button = gr.Button(value="Copy Settings")
|
||||
|
||||
def history_view_results( voice ):
|
||||
results = []
|
||||
files = []
|
||||
outdir = f"./results/{voice}/"
|
||||
for i, file in enumerate(os.listdir(outdir)):
|
||||
if file[-4:] != ".wav":
|
||||
continue
|
||||
|
||||
metadata, _ = read_generate_settings(f"{outdir}/{file}", save_latents=False)
|
||||
if metadata is None:
|
||||
continue
|
||||
|
||||
values = []
|
||||
for k in headers:
|
||||
v = file
|
||||
if k != "Name":
|
||||
v = metadata[headers[k]]
|
||||
values.append(v)
|
||||
|
||||
|
||||
files.append(file)
|
||||
results.append(values)
|
||||
|
||||
return (
|
||||
results,
|
||||
gr.Dropdown.update(choices=sorted(files))
|
||||
)
|
||||
|
||||
history_view_results_button.click(
|
||||
fn=history_view_results,
|
||||
inputs=history_voices,
|
||||
outputs=[
|
||||
history_info,
|
||||
history_results_list,
|
||||
]
|
||||
)
|
||||
history_view_result_button.click(
|
||||
fn=lambda voice, file: f"./results/{voice}/{file}",
|
||||
inputs=[
|
||||
history_voices,
|
||||
history_results_list,
|
||||
],
|
||||
outputs=history_audio
|
||||
)
|
||||
with gr.Tab("Utilities"):
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
audio_in = gr.File(type="file", label="Audio Input", file_types=["audio"])
|
||||
copy_button = gr.Button(value="Copy Settings")
|
||||
import_voice = gr.Button(value="Import Voice")
|
||||
with gr.Column():
|
||||
metadata_out = gr.JSON(label="Audio Metadata")
|
||||
latents_out = gr.File(type="binary", label="Voice Latents")
|
||||
|
||||
audio_in.upload(
|
||||
fn=read_generate_settings,
|
||||
inputs=audio_in,
|
||||
outputs=[
|
||||
metadata_out,
|
||||
latents_out
|
||||
]
|
||||
)
|
||||
|
||||
import_voice.click(
|
||||
fn=save_latents,
|
||||
inputs=audio_in,
|
||||
)
|
||||
with gr.Tab("Settings"):
|
||||
with gr.Row():
|
||||
exec_inputs = []
|
||||
with gr.Column():
|
||||
exec_inputs = exec_inputs + [
|
||||
gr.Textbox(label="Listen", value=args.listen, placeholder="127.0.0.1:7860/"),
|
||||
gr.Checkbox(label="Public Share Gradio", value=args.share),
|
||||
gr.Checkbox(label="Check For Updates", value=args.check_for_updates),
|
||||
gr.Checkbox(label="Only Load Models Locally", value=args.models_from_local_only),
|
||||
gr.Checkbox(label="Low VRAM", value=args.low_vram),
|
||||
gr.Checkbox(label="Embed Output Metadata", value=args.embed_output_metadata),
|
||||
gr.Checkbox(label="Slimmer Computed Latents", value=args.latents_lean_and_mean),
|
||||
]
|
||||
gr.Button(value="Check for Updates").click(check_for_updates)
|
||||
gr.Button(value="Reload TTS").click(reload_tts)
|
||||
with gr.Column():
|
||||
exec_inputs = exec_inputs + [
|
||||
gr.Number(label="Voice Latents Max Chunk Size", precision=0, value=args.cond_latent_max_chunk_size),
|
||||
gr.Number(label="Sample Batch Size", precision=0, value=args.sample_batch_size),
|
||||
gr.Number(label="Concurrency Count", precision=0, value=args.concurrency_count),
|
||||
gr.Number(label="Ouptut Sample Rate", precision=0, value=args.output_sample_rate),
|
||||
gr.Slider(label="Ouptut Volume", minimum=0, maximum=2, value=args.output_volume),
|
||||
]
|
||||
|
||||
for i in exec_inputs:
|
||||
i.change(
|
||||
fn=export_exec_settings,
|
||||
inputs=exec_inputs
|
||||
)
|
||||
with gr.Column():
|
||||
experimental_checkboxes = gr.CheckboxGroup(["Half Precision", "Conditioning-Free"], value=["Conditioning-Free"], label="Experimental Flags")
|
||||
cvvp_weight = gr.Slider(value=0, minimum=0, maximum=1, label="CVVP Weight")
|
||||
top_p = gr.Slider(value=0.8, minimum=0, maximum=1, label="Top P")
|
||||
diffusion_temperature = gr.Slider(value=1.0, minimum=0, maximum=1, label="Diffusion Temperature")
|
||||
length_penalty = gr.Slider(value=1.0, minimum=0, maximum=8, label="Length Penalty")
|
||||
repetition_penalty = gr.Slider(value=2.0, minimum=0, maximum=8, label="Repetition Penalty")
|
||||
cond_free_k = gr.Slider(value=2.0, minimum=0, maximum=4, label="Conditioning-Free K")
|
||||
|
||||
|
||||
input_settings = [
|
||||
text,
|
||||
delimiter,
|
||||
emotion,
|
||||
prompt,
|
||||
voice,
|
||||
mic_audio,
|
||||
seed,
|
||||
candidates,
|
||||
num_autoregressive_samples,
|
||||
diffusion_iterations,
|
||||
temperature,
|
||||
diffusion_sampler,
|
||||
breathing_room,
|
||||
cvvp_weight,
|
||||
top_p,
|
||||
diffusion_temperature,
|
||||
length_penalty,
|
||||
repetition_penalty,
|
||||
cond_free_k,
|
||||
experimental_checkboxes,
|
||||
]
|
||||
|
||||
submit_event = submit.click(generate,
|
||||
inputs=input_settings,
|
||||
outputs=[selected_voice, output_audio, generation_results],
|
||||
api_name='generate'
|
||||
)
|
||||
|
||||
copy_button.click(import_generate_settings,
|
||||
inputs=audio_in, # JSON elements cannot be used as inputs
|
||||
outputs=input_settings
|
||||
)
|
||||
|
||||
def history_copy_settings( voice, file ):
|
||||
settings = import_generate_settings( f"./results/{voice}/{file}" )
|
||||
return settings
|
||||
|
||||
history_copy_settings_button.click(history_copy_settings,
|
||||
inputs=[
|
||||
history_voices,
|
||||
history_results_list,
|
||||
],
|
||||
outputs=input_settings
|
||||
)
|
||||
|
||||
if os.path.isfile('./config/generate.json'):
|
||||
webui.load(import_generate_settings, inputs=None, outputs=input_settings)
|
||||
|
||||
if args.check_for_updates:
|
||||
webui.load(check_for_updates)
|
||||
|
||||
stop.click(fn=cancel_generate, inputs=None, outputs=None, cancels=[submit_event])
|
||||
|
||||
|
||||
webui.queue(concurrency_count=args.concurrency_count)
|
||||
|
||||
return webui
|
||||
Loading…
Reference in New Issue
Block a user