diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..cc86e35415d6fc4e76cbe0b9998a26248727ad4b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +ref_audio.wav filter=lfs diff=lfs merge=lfs -text +src/third_party/BigVGAN/demo/examples/megalovania_24k.wav filter=lfs diff=lfs merge=lfs -text +src/third_party/BigVGAN/filelists/LibriTTS/train-full.txt filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000000000000000000000000000000..7b092a60738af56c42808334fa2702135badfe26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: "Bug Report" +description: | + Please provide as much details to help address the issue, including logs and screenshots. +labels: + - bug +body: + - type: checkboxes + attributes: + label: Checks + description: "To ensure timely help, please confirm the following:" + options: + - label: This template is only for bug reports, usage problems go with 'Help Wanted'. + required: true + - label: I have thoroughly reviewed the project documentation but couldn't find information to solve my problem. + required: true + - label: I have searched for existing issues, including closed ones, and couldn't find a solution. + required: true + - label: I confirm that I am using English to submit this report in order to facilitate communication. + required: true + - type: textarea + attributes: + label: Environment Details + description: "Provide details such as OS, Python version, and any relevant software or dependencies." + placeholder: e.g., CentOS Linux 7, RTX 3090, Python 3.10, torch==2.3.0, cuda 11.8 + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: | + Include detailed steps, screenshots, and logs. Use the correct markdown syntax for code blocks. + placeholder: | + 1. Create a new conda environment. + 2. Clone the repository, install as local editable and properly set up. + 3. Run the command: `accelerate launch src/f5_tts/train/train.py`. + 4. Have following error message... (attach logs). + validations: + required: true + - type: textarea + attributes: + label: ✔️ Expected Behavior + placeholder: Describe what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: ❌ Actual Behavior + placeholder: Describe what actually happened. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000000000000000000000000000000000..4c0846bc12a64a2b60856e2cc9509073a4b1db30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,62 @@ +name: "Feature Request" +description: | + Some constructive suggestions and new ideas regarding current repo. +labels: + - enhancement +body: + - type: checkboxes + attributes: + label: Checks + description: "To help us grasp quickly, please confirm the following:" + options: + - label: This template is only for feature request. + required: true + - label: I have thoroughly reviewed the project documentation but couldn't find any relevant information that meets my needs. + required: true + - label: I have searched for existing issues, including closed ones, and found not discussion yet. + required: true + - label: I confirm that I am using English to submit this report in order to facilitate communication. + required: true + - type: textarea + attributes: + label: 1. Is this request related to a challenge you're experiencing? Tell us your story. + description: | + Describe the specific problem or scenario you're facing in detail. For example: + *"I was trying to use [feature] for [specific task], but encountered [issue]. This was frustrating because...."* + placeholder: Please describe the situation in as much detail as possible. + validations: + required: true + + - type: textarea + attributes: + label: 2. What is your suggested solution? + description: | + Provide a clear description of the feature or enhancement you'd like to propose. + How would this feature solve your issue or improve the project? + placeholder: Describe your idea or proposed solution here. + validations: + required: true + + - type: textarea + attributes: + label: 3. Additional context or comments + description: | + Any other relevant information, links, documents, or screenshots that provide clarity. + Use this section for anything not covered above. + placeholder: Add any extra details here. + validations: + required: false + + - type: checkboxes + attributes: + label: 4. Can you help us with this feature? + description: | + Let us know if you're interested in contributing. This is not a commitment but a way to express interest in collaboration. + options: + - label: I am interested in contributing to this feature. + required: false + + - type: markdown + attributes: + value: | + **Note:** Please submit only one request per issue to keep discussions focused and manageable. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/help_wanted.yml b/.github/ISSUE_TEMPLATE/help_wanted.yml new file mode 100644 index 0000000000000000000000000000000000000000..87efb3a1fa410f97dda8d419741e0efee77606cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help_wanted.yml @@ -0,0 +1,50 @@ +name: "Help Wanted" +description: | + Please provide as much details to help address the issue, including logs and screenshots. +labels: + - help wanted +body: + - type: checkboxes + attributes: + label: Checks + description: "To ensure timely help, please confirm the following:" + options: + - label: This template is only for usage issues encountered. + required: true + - label: I have thoroughly reviewed the project documentation but couldn't find information to solve my problem. + required: true + - label: I have searched for existing issues, including closed ones, and couldn't find a solution. + required: true + - label: I confirm that I am using English to submit this report in order to facilitate communication. + required: true + - type: textarea + attributes: + label: Environment Details + description: "Provide details such as OS, Python version, and any relevant software or dependencies." + placeholder: e.g., macOS 13.5, Python 3.10, torch==2.3.0, Gradio 4.44.1 + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: | + Include detailed steps, screenshots, and logs. Use the correct markdown syntax for code blocks. + placeholder: | + 1. Create a new conda environment. + 2. Clone the repository and install as pip package. + 3. Run the command: `f5-tts_infer-gradio` with no ref_text provided. + 4. Stuck there with the following message... (attach logs and also error msg e.g. after ctrl-c). + validations: + required: true + - type: textarea + attributes: + label: ✔️ Expected Behavior + placeholder: Describe what you expected to happen, e.g. output a generated audio + validations: + required: false + - type: textarea + attributes: + label: ❌ Actual Behavior + placeholder: Describe what actually happened, failure messages, etc. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000000000000000000000000000000000000..04b86629975c80a29ad17df39eb068eeb0601bb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,26 @@ +name: "Question" +description: | + Pure question or inquiry about the project. +labels: + - question +body: + - type: checkboxes + attributes: + label: Checks + description: "To help us grasp quickly, please confirm the following:" + options: + - label: This template is only for question, not feature requests or bug reports. + required: true + - label: I have thoroughly reviewed the project documentation and read the related paper(s). + required: true + - label: I have searched for existing issues, including closed ones, no similar questions. + required: true + - label: I confirm that I am using English to submit this report in order to facilitate communication. + required: true + - type: textarea + attributes: + label: Question details + description: | + Question details, clearly stated using proper markdown syntax. + validations: + required: true \ No newline at end of file diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2291902267f6961e252f7e91933deaf1a928e412 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml new file mode 100644 index 0000000000000000000000000000000000000000..502c149e90765f57621dd032f801e9aaf311ed9f --- /dev/null +++ b/.github/workflows/publish-docker-image.yaml @@ -0,0 +1,60 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ['main'] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧 + uses: jlumbroso/free-disk-space@main + with: + # This might remove tools that are actually needed, if set to "true" but frees about 6 GB + tool-cache: false + + # All of these default to true, but feel free to set to "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: false + docker-images: false + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/sync-hf.yaml b/.github/workflows/sync-hf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5d6fe33657b151351035fc7ac3d5d175175b325f --- /dev/null +++ b/.github/workflows/sync-hf.yaml @@ -0,0 +1,18 @@ +name: Sync to HF Space + +on: + push: + branches: + - main + +jobs: + trigger_curl: + runs-on: ubuntu-latest + + steps: + - name: Send cURL POST request + run: | + curl -X POST https://mrfakename-sync-f5.hf.space/gradio_api/call/refresh \ + -s \ + -H "Content-Type: application/json" \ + -d "{\"data\": [\"${{ secrets.REFRESH_PASSWORD }}\"]}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..744952816305f5bc74736755131aa074e3a37de3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +# Customed +.vscode/ +tests/ +runs/ +data/ +ckpts/ +wandb/ +results/ + + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..f6ed8f4c661131e538c06468cea7837ebccd1b24 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/third_party/BigVGAN"] + path = src/third_party/BigVGAN + url = https://github.com/NVIDIA/BigVGAN.git diff --git a/.gradio/certificate.pem b/.gradio/certificate.pem new file mode 100644 index 0000000000000000000000000000000000000000..b85c8037f6b60976b2546fdbae88312c5246d9a3 --- /dev/null +++ b/.gradio/certificate.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9d56c7c0156d996a8fae84ec049bb72a0ef1ca --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.7.0 + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..92f175d7c6abfe9df008a4094e6d5279a5ddf1e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM pytorch/pytorch:2.4.0-cuda12.4-cudnn9-devel + +USER root + +ARG DEBIAN_FRONTEND=noninteractive + +LABEL github_repo="https://github.com/SWivid/F5-TTS" + +RUN set -x \ + && apt-get update \ + && apt-get -y install wget curl man git less openssl libssl-dev unzip unar build-essential aria2 tmux vim \ + && apt-get install -y openssh-server sox libsox-fmt-all libsox-fmt-mp3 libsndfile1-dev ffmpeg \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +WORKDIR /workspace + +RUN git clone https://github.com/SWivid/F5-TTS.git \ + && cd F5-TTS \ + && pip install -e .[eval] + +ENV SHELL=/bin/bash + +WORKDIR /workspace/F5-TTS diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..71c49ce5bf7317f72f00f4ee17f03c174fd069ae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Yushen CHEN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4120e719c41fe010f08282725719729e2a6ef5e7..a1697d2543518cbea42774bc51f898daf987801b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,159 @@ ---- -title: Spanish F5 -emoji: 👀 -colorFrom: indigo -colorTo: pink -sdk: gradio -sdk_version: 5.9.1 -app_file: app.py -pinned: false -short_description: Spanish-F5 ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +--- +title: Spanish-F5 +app_file: app.py +sdk: gradio +sdk_version: 5.9.1 +--- +# F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching + +**F5-TTS**: Diffusion Transformer with ConvNeXt V2, faster trained and inference. + +**E2 TTS**: Flat-UNet Transformer, closest reproduction from [paper](https://arxiv.org/abs/2406.18009). + +**Sway Sampling**: Inference-time flow step sampling strategy, greatly improves performance + +### Thanks to all the contributors ! + +## News +Spanish model: https://huggingface.co/jpgallegoar/F5-Spanish/ + +## Installation + +```bash +# Create a python 3.10 conda env (you could also use virtualenv) +conda create -n f5-tts python=3.10 +conda activate f5-tts + +# Install pytorch with your CUDA version, e.g. +pip install torch==2.3.0+cu118 torchaudio==2.3.0+cu118 --extra-index-url https://download.pytorch.org/whl/cu118 +``` + +Then you can choose from a few options below: + +### 1. As a pip package (if just for inference) + +```bash +pip install git+https://github.com/jpgallegoar/Spanish-F5.git +``` + +### 2. Local editable (if also do training, finetuning) + +```bash +git clone https://github.com/jpgallegoar/Spanish-F5.git +cd F5-TTS +# git submodule update --init --recursive # (optional, if need bigvgan) +pip install -e . +``` +If initialize submodule, you should add the following code at the beginning of `src/third_party/BigVGAN/bigvgan.py`. +```python +import os +import sys +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +``` + +## Inference + +### 1. Gradio App + +Currently supported features: + +- Basic TTS with Chunk Inference +- Multi-Style / Multi-Speaker Generation +- Voice Chat powered by Qwen2.5-3B-Instruct + +```bash +# Launch a Gradio app (web interface) +f5-tts_infer-gradio + +# Specify the port/host +f5-tts_infer-gradio --port 7860 --host 0.0.0.0 + +# Launch a share link +f5-tts_infer-gradio --share +``` + +### 2. CLI Inference + +```bash +# Run with flags +# Leave --ref_text "" will have ASR model transcribe (extra GPU memory usage) +f5-tts_infer-cli \ +--model "F5-TTS" \ +--ref_audio "ref_audio.wav" \ +--ref_text "The content, subtitle or transcription of reference audio." \ +--gen_text "Some text you want TTS model generate for you." + +# Run with default setting. src/f5_tts/infer/examples/basic/basic.toml +f5-tts_infer-cli +# Or with your own .toml file +f5-tts_infer-cli -c custom.toml + +# Multi voice. See src/f5_tts/infer/README.md +f5-tts_infer-cli -c src/f5_tts/infer/examples/multi/story.toml +``` + +### 3. More instructions + +- In order to have better generation results, take a moment to read [detailed guidance](src/f5_tts/infer). +- The [Issues](https://github.com/SWivid/F5-TTS/issues?q=is%3Aissue) are very useful, please try to find the solution by properly searching the keywords of problem encountered. If no answer found, then feel free to open an issue. + + +## Training + +### 1. Gradio App + +Read [training & finetuning guidance](src/f5_tts/train) for more instructions. + +```bash +# Quick start with Gradio web interface +f5-tts_finetune-gradio +``` + + +## [Evaluation](src/f5_tts/eval) + + +## Development + +Use pre-commit to ensure code quality (will run linters and formatters automatically) + +```bash +pip install pre-commit +pre-commit install +``` + +When making a pull request, before each commit, run: + +```bash +pre-commit run --all-files +``` + +Note: Some model components have linting exceptions for E722 to accommodate tensor notation + + +## Acknowledgements + +- [E2-TTS](https://arxiv.org/abs/2406.18009) brilliant work, simple and effective +- [Emilia](https://arxiv.org/abs/2407.05361), [WenetSpeech4TTS](https://arxiv.org/abs/2406.05763) valuable datasets +- [lucidrains](https://github.com/lucidrains) initial CFM structure with also [bfs18](https://github.com/bfs18) for discussion +- [SD3](https://arxiv.org/abs/2403.03206) & [Hugging Face diffusers](https://github.com/huggingface/diffusers) DiT and MMDiT code structure +- [torchdiffeq](https://github.com/rtqichen/torchdiffeq) as ODE solver, [Vocos](https://huggingface.co/charactr/vocos-mel-24khz) as vocoder +- [FunASR](https://github.com/modelscope/FunASR), [faster-whisper](https://github.com/SYSTRAN/faster-whisper), [UniSpeech](https://github.com/microsoft/UniSpeech) for evaluation tools +- [ctc-forced-aligner](https://github.com/MahmoudAshraf97/ctc-forced-aligner) for speech edit test +- [mrfakename](https://x.com/realmrfakename) huggingface space demo ~ +- [f5-tts-mlx](https://github.com/lucasnewman/f5-tts-mlx/tree/main) Implementation with MLX framework by [Lucas Newman](https://github.com/lucasnewman) +- [F5-TTS-ONNX](https://github.com/DakeQQ/F5-TTS-ONNX) ONNX Runtime version by [DakeQQ](https://github.com/DakeQQ) + +## Citation +If our work and codebase is useful for you, please cite as: +``` +@article{chen-etal-2024-f5tts, + title={F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching}, + author={Yushen Chen and Zhikang Niu and Ziyang Ma and Keqi Deng and Chunhui Wang and Jian Zhao and Kai Yu and Xie Chen}, + journal={arXiv preprint arXiv:2410.06885}, + year={2024}, +} +``` +## License + +Our code is released under MIT License. The pre-trained models are licensed under the CC-BY-NC license due to the training data Emilia, which is an in-the-wild dataset. Sorry for any inconvenience this may cause. diff --git a/Spanish_F5.ipynb b/Spanish_F5.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..ee94d1e29fd8a0da571f3eef9f81b6298558cd47 --- /dev/null +++ b/Spanish_F5.ipynb @@ -0,0 +1,433 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0-duLUr_Iema", + "outputId": "615d30a6-2a04-4859-ad9d-79edd6a3ffc1" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Collecting git+https://github.com/jpgallegoar/Spanish-F5.git\n", + " Cloning https://github.com/jpgallegoar/Spanish-F5.git to /tmp/pip-req-build-mupnta95\n", + " Running command git clone --filter=blob:none --quiet https://github.com/jpgallegoar/Spanish-F5.git /tmp/pip-req-build-mupnta95\n", + " Resolved https://github.com/jpgallegoar/Spanish-F5.git to commit 840d95016e3dd838ee3e7bbe30c64fdced73a7ef\n", + " Running command git submodule update --init --recursive -q\n", + " Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n", + " Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n", + " Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + "Requirement already satisfied: accelerate>=0.33.0 in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.34.2)\n", + "Collecting bitsandbytes>0.37.0 (from f5-tts==0.0.0)\n", + " Downloading bitsandbytes-0.44.1-py3-none-manylinux_2_24_x86_64.whl.metadata (3.5 kB)\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.6.3-py3-none-any.whl.metadata (19 kB)\n", + "Requirement already satisfied: click in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (8.1.7)\n", + "Collecting datasets (from f5-tts==0.0.0)\n", + " Downloading datasets-3.1.0-py3-none-any.whl.metadata (20 kB)\n", + "Collecting ema-pytorch>=0.5.2 (from f5-tts==0.0.0)\n", + " Downloading ema_pytorch-0.7.3-py3-none-any.whl.metadata (691 bytes)\n", + "Collecting gradio>=3.45.2 (from f5-tts==0.0.0)\n", + " Downloading gradio-5.4.0-py3-none-any.whl.metadata (16 kB)\n", + "Requirement already satisfied: jieba in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.42.1)\n", + "Requirement already satisfied: librosa in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.10.2.post1)\n", + "Requirement already satisfied: matplotlib in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (3.8.0)\n", + "Requirement already satisfied: numpy<=1.26.4 in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (1.26.4)\n", + "Collecting pydub (from f5-tts==0.0.0)\n", + " Downloading pydub-0.25.1-py2.py3-none-any.whl.metadata (1.4 kB)\n", + "Collecting pypinyin (from f5-tts==0.0.0)\n", + " Downloading pypinyin-0.53.0-py2.py3-none-any.whl.metadata (12 kB)\n", + "Requirement already satisfied: safetensors in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.4.5)\n", + "Requirement already satisfied: soundfile in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.12.1)\n", + "Requirement already satisfied: tomli in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (2.0.2)\n", + "Requirement already satisfied: torch>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (2.5.0+cu121)\n", + "Requirement already satisfied: torchaudio>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (2.5.0+cu121)\n", + "Collecting torchdiffeq (from f5-tts==0.0.0)\n", + " Downloading torchdiffeq-0.2.4-py3-none-any.whl.metadata (440 bytes)\n", + "Requirement already satisfied: tqdm>=4.65.0 in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (4.66.6)\n", + "Requirement already satisfied: transformers in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (4.44.2)\n", + "Collecting transformers-stream-generator (from f5-tts==0.0.0)\n", + " Downloading transformers-stream-generator-0.0.5.tar.gz (13 kB)\n", + " Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + "Collecting vocos (from f5-tts==0.0.0)\n", + " Downloading vocos-0.1.0-py3-none-any.whl.metadata (4.8 kB)\n", + "Requirement already satisfied: wandb in /usr/local/lib/python3.10/dist-packages (from f5-tts==0.0.0) (0.18.5)\n", + "Collecting x-transformers>=1.31.14 (from f5-tts==0.0.0)\n", + " Downloading x_transformers-1.42.5-py3-none-any.whl.metadata (689 bytes)\n", + "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from accelerate>=0.33.0->f5-tts==0.0.0) (24.1)\n", + "Requirement already satisfied: psutil in /usr/local/lib/python3.10/dist-packages (from accelerate>=0.33.0->f5-tts==0.0.0) (5.9.5)\n", + "Requirement already satisfied: pyyaml in /usr/local/lib/python3.10/dist-packages (from accelerate>=0.33.0->f5-tts==0.0.0) (6.0.2)\n", + "Requirement already satisfied: huggingface-hub>=0.21.0 in /usr/local/lib/python3.10/dist-packages (from accelerate>=0.33.0->f5-tts==0.0.0) (0.24.7)\n", + "Collecting aiofiles<24.0,>=22.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading aiofiles-23.2.1-py3-none-any.whl.metadata (9.7 kB)\n", + "Requirement already satisfied: anyio<5.0,>=3.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (3.7.1)\n", + "Collecting fastapi<1.0,>=0.115.2 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading fastapi-0.115.4-py3-none-any.whl.metadata (27 kB)\n", + "Collecting ffmpy (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading ffmpy-0.4.0-py3-none-any.whl.metadata (2.9 kB)\n", + "Collecting gradio-client==1.4.2 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading gradio_client-1.4.2-py3-none-any.whl.metadata (7.1 kB)\n", + "Requirement already satisfied: httpx>=0.24.1 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (0.27.2)\n", + "Collecting huggingface-hub>=0.21.0 (from accelerate>=0.33.0->f5-tts==0.0.0)\n", + " Downloading huggingface_hub-0.26.2-py3-none-any.whl.metadata (13 kB)\n", + "Requirement already satisfied: jinja2<4.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (3.1.4)\n", + "Collecting markupsafe~=2.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.0 kB)\n", + "Requirement already satisfied: orjson~=3.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (3.10.10)\n", + "Requirement already satisfied: pandas<3.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (2.2.2)\n", + "Requirement already satisfied: pillow<12.0,>=8.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (10.4.0)\n", + "Requirement already satisfied: pydantic>=2.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (2.9.2)\n", + "Collecting python-multipart==0.0.12 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading python_multipart-0.0.12-py3-none-any.whl.metadata (1.9 kB)\n", + "Collecting ruff>=0.2.2 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (25 kB)\n", + "Collecting safehttpx<1.0,>=0.1.1 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading safehttpx-0.1.1-py3-none-any.whl.metadata (4.1 kB)\n", + "Collecting semantic-version~=2.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading semantic_version-2.10.0-py2.py3-none-any.whl.metadata (9.7 kB)\n", + "Collecting starlette<1.0,>=0.40.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading starlette-0.41.2-py3-none-any.whl.metadata (6.0 kB)\n", + "Collecting tomlkit==0.12.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading tomlkit-0.12.0-py3-none-any.whl.metadata (2.7 kB)\n", + "Requirement already satisfied: typer<1.0,>=0.12 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (0.12.5)\n", + "Requirement already satisfied: typing-extensions~=4.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (4.12.2)\n", + "Collecting uvicorn>=0.14.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading uvicorn-0.32.0-py3-none-any.whl.metadata (6.6 kB)\n", + "Requirement already satisfied: fsspec in /usr/local/lib/python3.10/dist-packages (from gradio-client==1.4.2->gradio>=3.45.2->f5-tts==0.0.0) (2024.10.0)\n", + "Collecting websockets<13.0,>=10.0 (from gradio-client==1.4.2->gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.6 kB)\n", + "Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from torch>=2.0.0->f5-tts==0.0.0) (3.16.1)\n", + "Requirement already satisfied: networkx in /usr/local/lib/python3.10/dist-packages (from torch>=2.0.0->f5-tts==0.0.0) (3.4.2)\n", + "Requirement already satisfied: sympy==1.13.1 in /usr/local/lib/python3.10/dist-packages (from torch>=2.0.0->f5-tts==0.0.0) (1.13.1)\n", + "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.10/dist-packages (from sympy==1.13.1->torch>=2.0.0->f5-tts==0.0.0) (1.3.0)\n", + "Collecting einx>=0.3.0 (from x-transformers>=1.31.14->f5-tts==0.0.0)\n", + " Downloading einx-0.3.0-py3-none-any.whl.metadata (6.9 kB)\n", + "Requirement already satisfied: einops>=0.8.0 in /usr/local/lib/python3.10/dist-packages (from x-transformers>=1.31.14->f5-tts==0.0.0) (0.8.0)\n", + "Requirement already satisfied: requests<3.0,>=2.0 in /usr/local/lib/python3.10/dist-packages (from cached-path->f5-tts==0.0.0) (2.32.3)\n", + "Requirement already satisfied: rich<14.0,>=12.1 in /usr/local/lib/python3.10/dist-packages (from cached-path->f5-tts==0.0.0) (13.9.3)\n", + "Collecting filelock (from torch>=2.0.0->f5-tts==0.0.0)\n", + " Downloading filelock-3.13.4-py3-none-any.whl.metadata (2.8 kB)\n", + "Collecting boto3<2.0,>=1.0 (from cached-path->f5-tts==0.0.0)\n", + " Downloading boto3-1.35.54-py3-none-any.whl.metadata (6.7 kB)\n", + "Requirement already satisfied: google-cloud-storage<3.0,>=1.32.0 in /usr/local/lib/python3.10/dist-packages (from cached-path->f5-tts==0.0.0) (2.8.0)\n", + "INFO: pip is looking at multiple versions of cached-path to determine which version is compatible with other requirements. This could take a while.\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.6.2-py3-none-any.whl.metadata (19 kB)\n", + " Downloading cached_path-1.6.0-py3-none-any.whl.metadata (19 kB)\n", + " Downloading cached_path-1.5.1-py3-none-any.whl.metadata (19 kB)\n", + "Collecting filelock (from torch>=2.0.0->f5-tts==0.0.0)\n", + " Downloading filelock-3.12.4-py3-none-any.whl.metadata (2.8 kB)\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.5.0-py3-none-any.whl.metadata (19 kB)\n", + " Downloading cached_path-1.4.0-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.3.5-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.3.4-py3-none-any.whl.metadata (6.3 kB)\n", + "Collecting filelock (from torch>=2.0.0->f5-tts==0.0.0)\n", + " Downloading filelock-3.9.1-py3-none-any.whl.metadata (2.4 kB)\n", + "INFO: pip is still looking at multiple versions of cached-path to determine which version is compatible with other requirements. This could take a while.\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.3.3-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.3.2-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.3.1-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.3.0-py3-none-any.whl.metadata (6.3 kB)\n", + " Downloading cached_path-1.2.0-py3-none-any.whl.metadata (6.0 kB)\n", + "INFO: This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. See https://pip.pypa.io/warnings/backtracking for guidance. If you want to abort this run, press Ctrl + C.\n", + " Downloading cached_path-1.1.6-py3-none-any.whl.metadata (6.0 kB)\n", + "Collecting rich<13.0,>=12.1 (from cached-path->f5-tts==0.0.0)\n", + " Downloading rich-12.6.0-py3-none-any.whl.metadata (18 kB)\n", + "Collecting filelock (from torch>=2.0.0->f5-tts==0.0.0)\n", + " Downloading filelock-3.8.2-py3-none-any.whl.metadata (2.3 kB)\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.1.5-py3-none-any.whl.metadata (6.0 kB)\n", + "Collecting filelock (from torch>=2.0.0->f5-tts==0.0.0)\n", + " Downloading filelock-3.7.1-py3-none-any.whl.metadata (2.5 kB)\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-1.1.4-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.1.3-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.1.2-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.1.1-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.1.0-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.0.2-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-1.0.1-py3-none-any.whl.metadata (5.9 kB)\n", + " Downloading cached_path-1.0.0-py3-none-any.whl.metadata (5.9 kB)\n", + " Downloading cached_path-0.3.4-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-0.3.3-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-0.3.2-py3-none-any.whl.metadata (6.0 kB)\n", + " Downloading cached_path-0.3.1-py3-none-any.whl.metadata (5.1 kB)\n", + " Downloading cached_path-0.3.0-py3-none-any.whl.metadata (4.1 kB)\n", + "Collecting google-cloud-storage<2.0,>=1.0 (from cached-path->f5-tts==0.0.0)\n", + " Downloading google_cloud_storage-1.44.0-py2.py3-none-any.whl.metadata (5.9 kB)\n", + "Collecting overrides<6.2,>=3.1 (from cached-path->f5-tts==0.0.0)\n", + " Downloading overrides-6.1.0-py3-none-any.whl.metadata (5.0 kB)\n", + "Collecting cached-path (from f5-tts==0.0.0)\n", + " Downloading cached_path-0.2.0-py3-none-any.whl.metadata (4.1 kB)\n", + " Downloading cached_path-0.1.0-py3-none-any.whl.metadata (3.4 kB)\n", + "Collecting overrides==3.1.0 (from cached-path->f5-tts==0.0.0)\n", + " Downloading overrides-3.1.0.tar.gz (11 kB)\n", + " Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + "Collecting gradio>=3.45.2 (from f5-tts==0.0.0)\n", + " Downloading gradio-5.3.0-py3-none-any.whl.metadata (15 kB)\n", + "Collecting python-multipart>=0.0.9 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading python_multipart-0.0.17-py3-none-any.whl.metadata (1.8 kB)\n", + "Collecting gradio>=3.45.2 (from f5-tts==0.0.0)\n", + " Downloading gradio-5.1.0-py3-none-any.whl.metadata (15 kB)\n", + "Collecting gradio-client==1.4.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading gradio_client-1.4.0-py3-none-any.whl.metadata (7.1 kB)\n", + "Collecting gradio>=3.45.2 (from f5-tts==0.0.0)\n", + " Downloading gradio-5.0.2-py3-none-any.whl.metadata (15 kB)\n", + " Downloading gradio-5.0.1-py3-none-any.whl.metadata (15 kB)\n", + " Downloading gradio-5.0.0-py3-none-any.whl.metadata (15 kB)\n", + " Downloading gradio-4.44.1-py3-none-any.whl.metadata (15 kB)\n", + "Collecting gradio-client==1.3.0 (from gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading gradio_client-1.3.0-py3-none-any.whl.metadata (7.1 kB)\n", + "Requirement already satisfied: importlib-resources<7.0,>=1.3 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (6.4.5)\n", + "Requirement already satisfied: urllib3~=2.0 in /usr/local/lib/python3.10/dist-packages (from gradio>=3.45.2->f5-tts==0.0.0) (2.2.3)\n", + "Collecting huggingface-hub>=0.21.0 (from accelerate>=0.33.0->f5-tts==0.0.0)\n", + " Downloading huggingface_hub-0.23.5-py3-none-any.whl.metadata (12 kB)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (1.3.0)\n", + "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (4.54.1)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (1.4.7)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (3.2.0)\n", + "Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib->f5-tts==0.0.0) (2.8.2)\n", + "Requirement already satisfied: pyarrow>=15.0.0 in /usr/local/lib/python3.10/dist-packages (from datasets->f5-tts==0.0.0) (17.0.0)\n", + "Collecting dill<0.3.9,>=0.3.0 (from datasets->f5-tts==0.0.0)\n", + " Downloading dill-0.3.8-py3-none-any.whl.metadata (10 kB)\n", + "Collecting xxhash (from datasets->f5-tts==0.0.0)\n", + " Downloading xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (12 kB)\n", + "Collecting multiprocess<0.70.17 (from datasets->f5-tts==0.0.0)\n", + " Downloading multiprocess-0.70.16-py310-none-any.whl.metadata (7.2 kB)\n", + "Collecting fsspec (from gradio-client==1.3.0->gradio>=3.45.2->f5-tts==0.0.0)\n", + " Downloading fsspec-2024.9.0-py3-none-any.whl.metadata (11 kB)\n", + "Requirement already satisfied: aiohttp in /usr/local/lib/python3.10/dist-packages (from datasets->f5-tts==0.0.0) (3.10.10)\n", + "Requirement already satisfied: audioread>=2.1.9 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (3.0.1)\n", + "Requirement already satisfied: scipy>=1.2.0 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (1.13.1)\n", + "Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (1.5.2)\n", + "Requirement already satisfied: joblib>=0.14 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (1.4.2)\n", + "Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (4.4.2)\n", + "Requirement already satisfied: numba>=0.51.0 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (0.60.0)\n", + "Requirement already satisfied: pooch>=1.1 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (1.8.2)\n", + "Requirement already satisfied: soxr>=0.3.2 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (0.5.0.post1)\n", + "Requirement already satisfied: lazy-loader>=0.1 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (0.4)\n", + "Requirement already satisfied: msgpack>=1.0 in /usr/local/lib/python3.10/dist-packages (from librosa->f5-tts==0.0.0) (1.1.0)\n", + "Requirement already satisfied: cffi>=1.0 in /usr/local/lib/python3.10/dist-packages (from soundfile->f5-tts==0.0.0) (1.17.1)\n", + "Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.10/dist-packages (from transformers->f5-tts==0.0.0) (2024.9.11)\n", + "Requirement already satisfied: tokenizers<0.20,>=0.19 in /usr/local/lib/python3.10/dist-packages (from transformers->f5-tts==0.0.0) (0.19.1)\n", + "Collecting encodec==0.1.1 (from vocos->f5-tts==0.0.0)\n", + " Downloading encodec-0.1.1.tar.gz (3.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3.7/3.7 MB\u001b[0m \u001b[31m32.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + "Requirement already satisfied: docker-pycreds>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (0.4.0)\n", + "Requirement already satisfied: gitpython!=3.1.29,>=1.0.0 in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (3.1.43)\n", + "Requirement already satisfied: platformdirs in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (4.3.6)\n", + "Requirement already satisfied: protobuf!=4.21.0,!=5.28.0,<6,>=3.19.0 in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (3.20.3)\n", + "Requirement already satisfied: sentry-sdk>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (2.17.0)\n", + "Requirement already satisfied: setproctitle in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (1.3.3)\n", + "Requirement already satisfied: setuptools in /usr/local/lib/python3.10/dist-packages (from wandb->f5-tts==0.0.0) (75.1.0)\n", + "Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio>=3.45.2->f5-tts==0.0.0) (3.10)\n", + "Requirement already satisfied: sniffio>=1.1 in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio>=3.45.2->f5-tts==0.0.0) (1.3.1)\n", + "Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio>=3.45.2->f5-tts==0.0.0) (1.2.2)\n", + "Collecting botocore<1.36.0,>=1.35.54 (from boto3<2.0,>=1.0->cached-path->f5-tts==0.0.0)\n", + " Downloading botocore-1.35.54-py3-none-any.whl.metadata (5.7 kB)\n", + "Collecting jmespath<2.0.0,>=0.7.1 (from boto3<2.0,>=1.0->cached-path->f5-tts==0.0.0)\n", + " Downloading jmespath-1.0.1-py3-none-any.whl.metadata (7.6 kB)\n", + "Collecting s3transfer<0.11.0,>=0.10.0 (from boto3<2.0,>=1.0->cached-path->f5-tts==0.0.0)\n", + " Downloading s3transfer-0.10.3-py3-none-any.whl.metadata (1.7 kB)\n", + "Requirement already satisfied: pycparser in /usr/local/lib/python3.10/dist-packages (from cffi>=1.0->soundfile->f5-tts==0.0.0) (2.22)\n", + "Requirement already satisfied: six>=1.4.0 in /usr/local/lib/python3.10/dist-packages (from docker-pycreds>=0.4.0->wandb->f5-tts==0.0.0) (1.16.0)\n", + "Requirement already satisfied: frozendict in /usr/local/lib/python3.10/dist-packages (from einx>=0.3.0->x-transformers>=1.31.14->f5-tts==0.0.0) (2.4.6)\n", + "Requirement already satisfied: aiohappyeyeballs>=2.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (2.4.3)\n", + "Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (1.3.1)\n", + "Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (24.2.0)\n", + "Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (1.5.0)\n", + "Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (6.1.0)\n", + "Requirement already satisfied: yarl<2.0,>=1.12.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (1.17.0)\n", + "Requirement already satisfied: async-timeout<5.0,>=4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp->datasets->f5-tts==0.0.0) (4.0.3)\n", + "Requirement already satisfied: gitdb<5,>=4.0.1 in /usr/local/lib/python3.10/dist-packages (from gitpython!=3.1.29,>=1.0.0->wandb->f5-tts==0.0.0) (4.0.11)\n", + "Requirement already satisfied: google-auth<3.0dev,>=1.25.0 in /usr/local/lib/python3.10/dist-packages (from google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (2.27.0)\n", + "Requirement already satisfied: google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0dev,>=1.31.5 in /usr/local/lib/python3.10/dist-packages (from google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (2.19.2)\n", + "Requirement already satisfied: google-cloud-core<3.0dev,>=2.3.0 in /usr/local/lib/python3.10/dist-packages (from google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (2.4.1)\n", + "Requirement already satisfied: google-resumable-media>=2.3.2 in /usr/local/lib/python3.10/dist-packages (from google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (2.7.2)\n", + "Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from httpx>=0.24.1->gradio>=3.45.2->f5-tts==0.0.0) (2024.8.30)\n", + "Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.10/dist-packages (from httpx>=0.24.1->gradio>=3.45.2->f5-tts==0.0.0) (1.0.6)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /usr/local/lib/python3.10/dist-packages (from httpcore==1.*->httpx>=0.24.1->gradio>=3.45.2->f5-tts==0.0.0) (0.14.0)\n", + "Requirement already satisfied: llvmlite<0.44,>=0.43.0dev0 in /usr/local/lib/python3.10/dist-packages (from numba>=0.51.0->librosa->f5-tts==0.0.0) (0.43.0)\n", + "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas<3.0,>=1.0->gradio>=3.45.2->f5-tts==0.0.0) (2024.2)\n", + "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.10/dist-packages (from pandas<3.0,>=1.0->gradio>=3.45.2->f5-tts==0.0.0) (2024.2)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.0->gradio>=3.45.2->f5-tts==0.0.0) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.23.4 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.0->gradio>=3.45.2->f5-tts==0.0.0) (2.23.4)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests<3.0,>=2.0->cached-path->f5-tts==0.0.0) (3.4.0)\n", + "Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.10/dist-packages (from rich<14.0,>=12.1->cached-path->f5-tts==0.0.0) (3.0.0)\n", + "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.10/dist-packages (from rich<14.0,>=12.1->cached-path->f5-tts==0.0.0) (2.18.0)\n", + "Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->librosa->f5-tts==0.0.0) (3.5.0)\n", + "Requirement already satisfied: shellingham>=1.3.0 in /usr/local/lib/python3.10/dist-packages (from typer<1.0,>=0.12->gradio>=3.45.2->f5-tts==0.0.0) (1.5.4)\n", + "Requirement already satisfied: smmap<6,>=3.0.1 in /usr/local/lib/python3.10/dist-packages (from gitdb<5,>=4.0.1->gitpython!=3.1.29,>=1.0.0->wandb->f5-tts==0.0.0) (5.0.1)\n", + "Requirement already satisfied: googleapis-common-protos<2.0.dev0,>=1.56.2 in /usr/local/lib/python3.10/dist-packages (from google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0dev,>=1.31.5->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (1.65.0)\n", + "Requirement already satisfied: proto-plus<2.0.0dev,>=1.22.3 in /usr/local/lib/python3.10/dist-packages (from google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0dev,>=1.31.5->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (1.25.0)\n", + "Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from google-auth<3.0dev,>=1.25.0->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (5.5.0)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.10/dist-packages (from google-auth<3.0dev,>=1.25.0->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (0.4.1)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.10/dist-packages (from google-auth<3.0dev,>=1.25.0->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (4.9)\n", + "Requirement already satisfied: google-crc32c<2.0dev,>=1.0 in /usr/local/lib/python3.10/dist-packages (from google-resumable-media>=2.3.2->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (1.6.0)\n", + "Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.10/dist-packages (from markdown-it-py>=2.2.0->rich<14.0,>=12.1->cached-path->f5-tts==0.0.0) (0.1.2)\n", + "Requirement already satisfied: propcache>=0.2.0 in /usr/local/lib/python3.10/dist-packages (from yarl<2.0,>=1.12.0->aiohttp->datasets->f5-tts==0.0.0) (0.2.0)\n", + "Requirement already satisfied: pyasn1<0.7.0,>=0.4.6 in /usr/local/lib/python3.10/dist-packages (from pyasn1-modules>=0.2.1->google-auth<3.0dev,>=1.25.0->google-cloud-storage<3.0,>=1.32.0->cached-path->f5-tts==0.0.0) (0.6.1)\n", + "Downloading bitsandbytes-0.44.1-py3-none-manylinux_2_24_x86_64.whl (122.4 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m122.4/122.4 MB\u001b[0m \u001b[31m6.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading ema_pytorch-0.7.3-py3-none-any.whl (9.6 kB)\n", + "Downloading gradio-4.44.1-py3-none-any.whl (18.1 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m18.1/18.1 MB\u001b[0m \u001b[31m53.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading gradio_client-1.3.0-py3-none-any.whl (318 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m318.7/318.7 kB\u001b[0m \u001b[31m17.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading tomlkit-0.12.0-py3-none-any.whl (37 kB)\n", + "Downloading cached_path-1.6.3-py3-none-any.whl (35 kB)\n", + "Downloading huggingface_hub-0.23.5-py3-none-any.whl (402 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m402.8/402.8 kB\u001b[0m \u001b[31m20.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading x_transformers-1.42.5-py3-none-any.whl (47 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m47.5/47.5 kB\u001b[0m \u001b[31m3.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading datasets-3.1.0-py3-none-any.whl (480 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m480.6/480.6 kB\u001b[0m \u001b[31m23.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading pydub-0.25.1-py2.py3-none-any.whl (32 kB)\n", + "Downloading pypinyin-0.53.0-py2.py3-none-any.whl (834 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m834.7/834.7 kB\u001b[0m \u001b[31m32.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading torchdiffeq-0.2.4-py3-none-any.whl (32 kB)\n", + "Downloading vocos-0.1.0-py3-none-any.whl (24 kB)\n", + "Downloading aiofiles-23.2.1-py3-none-any.whl (15 kB)\n", + "Downloading boto3-1.35.54-py3-none-any.whl (139 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m139.2/139.2 kB\u001b[0m \u001b[31m9.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading dill-0.3.8-py3-none-any.whl (116 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m116.3/116.3 kB\u001b[0m \u001b[31m7.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading einx-0.3.0-py3-none-any.whl (102 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m103.0/103.0 kB\u001b[0m \u001b[31m5.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading fastapi-0.115.4-py3-none-any.whl (94 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m94.7/94.7 kB\u001b[0m \u001b[31m4.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading filelock-3.13.4-py3-none-any.whl (11 kB)\n", + "Downloading fsspec-2024.9.0-py3-none-any.whl (179 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m179.3/179.3 kB\u001b[0m \u001b[31m9.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)\n", + "Downloading multiprocess-0.70.16-py310-none-any.whl (134 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m134.8/134.8 kB\u001b[0m \u001b[31m9.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading python_multipart-0.0.17-py3-none-any.whl (24 kB)\n", + "Downloading ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.0 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.0/11.0 MB\u001b[0m \u001b[31m90.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading semantic_version-2.10.0-py2.py3-none-any.whl (15 kB)\n", + "Downloading uvicorn-0.32.0-py3-none-any.whl (63 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m63.7/63.7 kB\u001b[0m \u001b[31m4.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading ffmpy-0.4.0-py3-none-any.whl (5.8 kB)\n", + "Downloading xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (194 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m194.1/194.1 kB\u001b[0m \u001b[31m12.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading botocore-1.35.54-py3-none-any.whl (12.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m12.7/12.7 MB\u001b[0m \u001b[31m78.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading jmespath-1.0.1-py3-none-any.whl (20 kB)\n", + "Downloading s3transfer-0.10.3-py3-none-any.whl (82 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m82.6/82.6 kB\u001b[0m \u001b[31m5.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading starlette-0.41.2-py3-none-any.whl (73 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m73.3/73.3 kB\u001b[0m \u001b[31m5.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hDownloading websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (130 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m130.2/130.2 kB\u001b[0m \u001b[31m6.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hBuilding wheels for collected packages: f5-tts, transformers-stream-generator, encodec\n", + " Building wheel for f5-tts (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for f5-tts: filename=f5_tts-0.0.0-py3-none-any.whl size=1280758 sha256=b900d02175b3eccb21dec0d1801d9bffc4fd30451509a67ef45a6f0653580c7c\n", + " Stored in directory: /tmp/pip-ephem-wheel-cache-3_lciyxv/wheels/63/43/c5/fb04687e74a83e9bc15bb575c33e53b6aca4bf7a2fb32982f2\n", + " Building wheel for transformers-stream-generator (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for transformers-stream-generator: filename=transformers_stream_generator-0.0.5-py3-none-any.whl size=12425 sha256=0e9f6ab066ff4bcfa59962bd47464e4dc82ea27afed7086565aad6d86b0b6e5b\n", + " Stored in directory: /root/.cache/pip/wheels/95/4a/90/140f7b67d125906f6a165f38aad212ecb4a695ad0d87582437\n", + " Building wheel for encodec (setup.py) ... \u001b[?25l\u001b[?25hdone\n", + " Created wheel for encodec: filename=encodec-0.1.1-py3-none-any.whl size=45760 sha256=bccc14b58f86cf341371dda33deedf0bd6ef90e06649662b5337eb0e9e785a92\n", + " Stored in directory: /root/.cache/pip/wheels/fc/36/cb/81af8b985a5f5e0815312d5e52b41263237af07b977e6bcbf3\n", + "Successfully built f5-tts transformers-stream-generator encodec\n", + "Installing collected packages: pydub, xxhash, websockets, uvicorn, tomlkit, semantic-version, ruff, python-multipart, pypinyin, markupsafe, jmespath, fsspec, filelock, ffmpy, dill, aiofiles, starlette, multiprocess, huggingface-hub, einx, botocore, s3transfer, gradio-client, fastapi, x-transformers, torchdiffeq, gradio, ema-pytorch, boto3, bitsandbytes, transformers-stream-generator, encodec, datasets, vocos, cached-path, f5-tts\n", + " Attempting uninstall: markupsafe\n", + " Found existing installation: MarkupSafe 3.0.2\n", + " Uninstalling MarkupSafe-3.0.2:\n", + " Successfully uninstalled MarkupSafe-3.0.2\n", + " Attempting uninstall: fsspec\n", + " Found existing installation: fsspec 2024.10.0\n", + " Uninstalling fsspec-2024.10.0:\n", + " Successfully uninstalled fsspec-2024.10.0\n", + " Attempting uninstall: filelock\n", + " Found existing installation: filelock 3.16.1\n", + " Uninstalling filelock-3.16.1:\n", + " Successfully uninstalled filelock-3.16.1\n", + " Attempting uninstall: huggingface-hub\n", + " Found existing installation: huggingface-hub 0.24.7\n", + " Uninstalling huggingface-hub-0.24.7:\n", + " Successfully uninstalled huggingface-hub-0.24.7\n", + "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "gcsfs 2024.10.0 requires fsspec==2024.10.0, but you have fsspec 2024.9.0 which is incompatible.\n", + "pytensor 2.25.5 requires filelock>=3.15, but you have filelock 3.13.4 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0mSuccessfully installed aiofiles-23.2.1 bitsandbytes-0.44.1 boto3-1.35.54 botocore-1.35.54 cached-path-1.6.3 datasets-3.1.0 dill-0.3.8 einx-0.3.0 ema-pytorch-0.7.3 encodec-0.1.1 f5-tts-0.0.0 fastapi-0.115.4 ffmpy-0.4.0 filelock-3.13.4 fsspec-2024.9.0 gradio-4.44.1 gradio-client-1.3.0 huggingface-hub-0.23.5 jmespath-1.0.1 markupsafe-2.1.5 multiprocess-0.70.16 pydub-0.25.1 pypinyin-0.53.0 python-multipart-0.0.17 ruff-0.7.2 s3transfer-0.10.3 semantic-version-2.10.0 starlette-0.41.2 tomlkit-0.12.0 torchdiffeq-0.2.4 transformers-stream-generator-0.0.5 uvicorn-0.32.0 vocos-0.1.0 websockets-12.0 x-transformers-1.42.5 xxhash-3.5.0\n" + ] + } + ], + "source": [ + "pip install git+https://github.com/jpgallegoar/Spanish-F5.git" + ] + }, + { + "cell_type": "code", + "source": [ + "!f5-tts_infer-gradio" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "a2LYVge6IhOh", + "outputId": "be4d8d64-c020-4c97-eb57-ed0ceced7fba" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "2024-11-04 17:05:51.302161: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-11-04 17:05:51.328444: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-11-04 17:05:51.335942: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", + "2024-11-04 17:05:51.354757: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2024-11-04 17:05:52.875946: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n", + "Download Vocos from huggingface charactr/vocos-mel-24khz\n", + "config.yaml: 100% 461/461 [00:00<00:00, 2.61MB/s]\n", + "pytorch_model.bin: 100% 54.4M/54.4M [00:00<00:00, 139MB/s]\n", + "/usr/local/lib/python3.10/dist-packages/vocos/pretrained.py:70: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n", + " state_dict = torch.load(model_path, map_location=\"cpu\")\n", + "model_1200000.safetensors: 100% 1.35G/1.35G [02:09<00:00, 10.4MB/s]\n", + "\n", + "vocab : /usr/local/lib/python3.10/dist-packages/f5_tts/infer/examples/vocab.txt\n", + "tokenizer : custom\n", + "model : /root/.cache/huggingface/hub/models--jpgallegoar--F5-Spanish/snapshots/120ddcfa7813b928325a787b9ad2bf038d2c32df/model_1200000.safetensors \n", + "\n", + "Starting app...\n", + "Running on local URL: http://127.0.0.1:7860\n", + "Running on public URL: https://dd1c9f211495273ff6.gradio.live\n", + "\n", + "This share link expires in 72 hours. For free permanent hosting and GPU upgrades, run `gradio deploy` from Terminal to deploy to Spaces (https://huggingface.co/spaces)\n", + "config.json: 100% 1.26k/1.26k [00:00<00:00, 1.89MB/s]\n", + "model.safetensors: 21% 336M/1.62G [00:32<02:03, 10.4MB/s]" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..659b2282efe3bd7bb626df7c56b7a4dbb8d17fc6 --- /dev/null +++ b/app.py @@ -0,0 +1,7 @@ +import gradio as gr + +def greet(name): + return f"Hola {name}!" + +iface = gr.Interface(fn=greet, inputs="text", outputs="text") +iface.launch() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..e9a5312989913ea75eb7822158b4696fe9ab6915 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["setuptools >= 61.0", "setuptools-scm>=8.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "f5-tts" +dynamic = ["version"] +description = "F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching" +readme = "README.md" +license = {text = "MIT License"} +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", +] +dependencies = [ + "accelerate>=0.33.0", + "bitsandbytes==0.44.1", + "cached_path", + "click", + "datasets", + "ema_pytorch>=0.5.2", + "gradio>=3.45.2", + "jieba", + "librosa", + "matplotlib", + "numpy<=1.26.4", + "pydub", + "pypinyin", + "safetensors", + "soundfile", + "tomli", + "torch>=2.0.0", + "torchaudio>=2.0.0", + "torchdiffeq", + "tqdm>=4.65.0", + "transformers", + "transformers_stream_generator", + "vocos", + "wandb", + "x_transformers>=1.31.14", + "num2words", +] + +[project.optional-dependencies] +eval = [ + "faster_whisper==0.10.1", + "funasr", + "jiwer", + "modelscope", + "zhconv", + "zhon", +] + +[project.urls] +Homepage = "https://github.com/SWivid/F5-TTS" + +[project.scripts] +"f5-tts_infer-cli" = "f5_tts.infer.infer_cli:main" +"f5-tts_infer-gradio" = "f5_tts.infer.infer_gradio:main" +"f5-tts_finetune-cli" = "f5_tts.train.finetune_cli:main" +"f5-tts_finetune-gradio" = "f5_tts.train.finetune_gradio:main" diff --git a/ref_audio.wav b/ref_audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..9a228550ecee6dadddcaf4c1d226db0981873a12 --- /dev/null +++ b/ref_audio.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43beaf8e4f7097eff57943f10f166561e4dfbfb8fe34fafd188e2b123468eebc +size 1441870 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000000000000000000000000000000000..55748b19a9737b28f1dfcd9a35738f2d06c6df52 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,10 @@ +line-length = 120 +target-version = "py310" + +[lint] +# Only ignore variables with names starting with "_". +dummy-variable-rgx = "^_.*$" + +[lint.isort] +force-single-line = true +lines-after-imports = 2 diff --git a/src/f5_tts.egg-info/PKG-INFO b/src/f5_tts.egg-info/PKG-INFO new file mode 100644 index 0000000000000000000000000000000000000000..88821f6f1c16fbfb837340a90f9c0b3198ab36a3 --- /dev/null +++ b/src/f5_tts.egg-info/PKG-INFO @@ -0,0 +1,198 @@ +Metadata-Version: 2.1 +Name: f5-tts +Version: 0.0.0 +Summary: F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching +License: MIT License +Project-URL: Homepage, https://github.com/SWivid/F5-TTS +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: accelerate>=0.33.0 +Requires-Dist: bitsandbytes==0.44.1 +Requires-Dist: cached_path +Requires-Dist: click +Requires-Dist: datasets +Requires-Dist: ema_pytorch>=0.5.2 +Requires-Dist: gradio>=3.45.2 +Requires-Dist: jieba +Requires-Dist: librosa +Requires-Dist: matplotlib +Requires-Dist: numpy<=1.26.4 +Requires-Dist: pydub +Requires-Dist: pypinyin +Requires-Dist: safetensors +Requires-Dist: soundfile +Requires-Dist: tomli +Requires-Dist: torch>=2.0.0 +Requires-Dist: torchaudio>=2.0.0 +Requires-Dist: torchdiffeq +Requires-Dist: tqdm>=4.65.0 +Requires-Dist: transformers +Requires-Dist: transformers_stream_generator +Requires-Dist: vocos +Requires-Dist: wandb +Requires-Dist: x_transformers>=1.31.14 +Requires-Dist: num2words +Provides-Extra: eval +Requires-Dist: faster_whisper==0.10.1; extra == "eval" +Requires-Dist: funasr; extra == "eval" +Requires-Dist: jiwer; extra == "eval" +Requires-Dist: modelscope; extra == "eval" +Requires-Dist: zhconv; extra == "eval" +Requires-Dist: zhon; extra == "eval" + +# F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching + +**F5-TTS**: Diffusion Transformer with ConvNeXt V2, faster trained and inference. + +**E2 TTS**: Flat-UNet Transformer, closest reproduction from [paper](https://arxiv.org/abs/2406.18009). + +**Sway Sampling**: Inference-time flow step sampling strategy, greatly improves performance + +### Thanks to all the contributors ! + +## News +Spanish model: https://huggingface.co/jpgallegoar/F5-Spanish/ + +## Installation + +```bash +# Create a python 3.10 conda env (you could also use virtualenv) +conda create -n f5-tts python=3.10 +conda activate f5-tts + +# Install pytorch with your CUDA version, e.g. +pip install torch==2.3.0+cu118 torchaudio==2.3.0+cu118 --extra-index-url https://download.pytorch.org/whl/cu118 +``` + +Then you can choose from a few options below: + +### 1. As a pip package (if just for inference) + +```bash +pip install git+https://github.com/jpgallegoar/Spanish-F5.git +``` + +### 2. Local editable (if also do training, finetuning) + +```bash +git clone https://github.com/jpgallegoar/Spanish-F5.git +cd F5-TTS +# git submodule update --init --recursive # (optional, if need bigvgan) +pip install -e . +``` +If initialize submodule, you should add the following code at the beginning of `src/third_party/BigVGAN/bigvgan.py`. +```python +import os +import sys +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +``` + +## Inference + +### 1. Gradio App + +Currently supported features: + +- Basic TTS with Chunk Inference +- Multi-Style / Multi-Speaker Generation +- Voice Chat powered by Qwen2.5-3B-Instruct + +```bash +# Launch a Gradio app (web interface) +f5-tts_infer-gradio + +# Specify the port/host +f5-tts_infer-gradio --port 7860 --host 0.0.0.0 + +# Launch a share link +f5-tts_infer-gradio --share +``` + +### 2. CLI Inference + +```bash +# Run with flags +# Leave --ref_text "" will have ASR model transcribe (extra GPU memory usage) +f5-tts_infer-cli \ +--model "F5-TTS" \ +--ref_audio "ref_audio.wav" \ +--ref_text "The content, subtitle or transcription of reference audio." \ +--gen_text "Some text you want TTS model generate for you." + +# Run with default setting. src/f5_tts/infer/examples/basic/basic.toml +f5-tts_infer-cli +# Or with your own .toml file +f5-tts_infer-cli -c custom.toml + +# Multi voice. See src/f5_tts/infer/README.md +f5-tts_infer-cli -c src/f5_tts/infer/examples/multi/story.toml +``` + +### 3. More instructions + +- In order to have better generation results, take a moment to read [detailed guidance](src/f5_tts/infer). +- The [Issues](https://github.com/SWivid/F5-TTS/issues?q=is%3Aissue) are very useful, please try to find the solution by properly searching the keywords of problem encountered. If no answer found, then feel free to open an issue. + + +## Training + +### 1. Gradio App + +Read [training & finetuning guidance](src/f5_tts/train) for more instructions. + +```bash +# Quick start with Gradio web interface +f5-tts_finetune-gradio +``` + + +## [Evaluation](src/f5_tts/eval) + + +## Development + +Use pre-commit to ensure code quality (will run linters and formatters automatically) + +```bash +pip install pre-commit +pre-commit install +``` + +When making a pull request, before each commit, run: + +```bash +pre-commit run --all-files +``` + +Note: Some model components have linting exceptions for E722 to accommodate tensor notation + + +## Acknowledgements + +- [E2-TTS](https://arxiv.org/abs/2406.18009) brilliant work, simple and effective +- [Emilia](https://arxiv.org/abs/2407.05361), [WenetSpeech4TTS](https://arxiv.org/abs/2406.05763) valuable datasets +- [lucidrains](https://github.com/lucidrains) initial CFM structure with also [bfs18](https://github.com/bfs18) for discussion +- [SD3](https://arxiv.org/abs/2403.03206) & [Hugging Face diffusers](https://github.com/huggingface/diffusers) DiT and MMDiT code structure +- [torchdiffeq](https://github.com/rtqichen/torchdiffeq) as ODE solver, [Vocos](https://huggingface.co/charactr/vocos-mel-24khz) as vocoder +- [FunASR](https://github.com/modelscope/FunASR), [faster-whisper](https://github.com/SYSTRAN/faster-whisper), [UniSpeech](https://github.com/microsoft/UniSpeech) for evaluation tools +- [ctc-forced-aligner](https://github.com/MahmoudAshraf97/ctc-forced-aligner) for speech edit test +- [mrfakename](https://x.com/realmrfakename) huggingface space demo ~ +- [f5-tts-mlx](https://github.com/lucasnewman/f5-tts-mlx/tree/main) Implementation with MLX framework by [Lucas Newman](https://github.com/lucasnewman) +- [F5-TTS-ONNX](https://github.com/DakeQQ/F5-TTS-ONNX) ONNX Runtime version by [DakeQQ](https://github.com/DakeQQ) + +## Citation +If our work and codebase is useful for you, please cite as: +``` +@article{chen-etal-2024-f5tts, + title={F5-TTS: A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching}, + author={Yushen Chen and Zhikang Niu and Ziyang Ma and Keqi Deng and Chunhui Wang and Jian Zhao and Kai Yu and Xie Chen}, + journal={arXiv preprint arXiv:2410.06885}, + year={2024}, +} +``` +## License + +Our code is released under MIT License. The pre-trained models are licensed under the CC-BY-NC license due to the training data Emilia, which is an in-the-wild dataset. Sorry for any inconvenience this may cause. diff --git a/src/f5_tts.egg-info/SOURCES.txt b/src/f5_tts.egg-info/SOURCES.txt new file mode 100644 index 0000000000000000000000000000000000000000..2d459d0d49c01430a6e80cefea8c3011d3b3f240 --- /dev/null +++ b/src/f5_tts.egg-info/SOURCES.txt @@ -0,0 +1,67 @@ +.gitignore +.gitmodules +.pre-commit-config.yaml +Dockerfile +LICENSE +README.md +Spanish_F5.ipynb +pyproject.toml +ruff.toml +.github/ISSUE_TEMPLATE/bug_report.yml +.github/ISSUE_TEMPLATE/feature_request.yml +.github/ISSUE_TEMPLATE/help_wanted.yml +.github/ISSUE_TEMPLATE/question.yml +.github/workflows/pre-commit.yaml +.github/workflows/publish-docker-image.yaml +.github/workflows/sync-hf.yaml +ckpts/README.md +data/librispeech_pc_test_clean_cross_sentence.lst +data/Emilia_ZH_EN_pinyin/vocab.txt +src/f5_tts/api.py +src/f5_tts/socket_server.py +src/f5_tts.egg-info/PKG-INFO +src/f5_tts.egg-info/SOURCES.txt +src/f5_tts.egg-info/dependency_links.txt +src/f5_tts.egg-info/entry_points.txt +src/f5_tts.egg-info/requires.txt +src/f5_tts.egg-info/top_level.txt +src/f5_tts/eval/README.md +src/f5_tts/eval/ecapa_tdnn.py +src/f5_tts/eval/eval_infer_batch.py +src/f5_tts/eval/eval_infer_batch.sh +src/f5_tts/eval/eval_librispeech_test_clean.py +src/f5_tts/eval/eval_seedtts_testset.py +src/f5_tts/eval/utils_eval.py +src/f5_tts/infer/README.md +src/f5_tts/infer/infer_cli.py +src/f5_tts/infer/infer_gradio.py +src/f5_tts/infer/speech_edit.py +src/f5_tts/infer/utils_infer.py +src/f5_tts/infer/examples/vocab.txt +src/f5_tts/infer/examples/basic/basic.toml +src/f5_tts/infer/examples/basic/basic_ref_en.wav +src/f5_tts/infer/examples/basic/basic_ref_zh.wav +src/f5_tts/infer/examples/multi/country.flac +src/f5_tts/infer/examples/multi/main.flac +src/f5_tts/infer/examples/multi/story.toml +src/f5_tts/infer/examples/multi/story.txt +src/f5_tts/infer/examples/multi/town.flac +src/f5_tts/model/__init__.py +src/f5_tts/model/cfm.py +src/f5_tts/model/dataset.py +src/f5_tts/model/modules.py +src/f5_tts/model/trainer.py +src/f5_tts/model/utils.py +src/f5_tts/model/backbones/README.md +src/f5_tts/model/backbones/dit.py +src/f5_tts/model/backbones/mmdit.py +src/f5_tts/model/backbones/unett.py +src/f5_tts/scripts/count_max_epoch.py +src/f5_tts/scripts/count_params_gflops.py +src/f5_tts/train/README.md +src/f5_tts/train/finetune_cli.py +src/f5_tts/train/finetune_gradio.py +src/f5_tts/train/train.py +src/f5_tts/train/datasets/prepare_csv_wavs.py +src/f5_tts/train/datasets/prepare_emilia.py +src/f5_tts/train/datasets/prepare_wenetspeech4tts.py \ No newline at end of file diff --git a/src/f5_tts.egg-info/dependency_links.txt b/src/f5_tts.egg-info/dependency_links.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/src/f5_tts.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/f5_tts.egg-info/entry_points.txt b/src/f5_tts.egg-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae1d4a47feee181ad63d2d137d0d9f620a8611b6 --- /dev/null +++ b/src/f5_tts.egg-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +f5-tts_finetune-cli = f5_tts.train.finetune_cli:main +f5-tts_finetune-gradio = f5_tts.train.finetune_gradio:main +f5-tts_infer-cli = f5_tts.infer.infer_cli:main +f5-tts_infer-gradio = f5_tts.infer.infer_gradio:main diff --git a/src/f5_tts.egg-info/requires.txt b/src/f5_tts.egg-info/requires.txt new file mode 100644 index 0000000000000000000000000000000000000000..d6dbf2003bc5fceed9bce96717356b3c353a4d18 --- /dev/null +++ b/src/f5_tts.egg-info/requires.txt @@ -0,0 +1,34 @@ +accelerate>=0.33.0 +bitsandbytes==0.44.1 +cached_path +click +datasets +ema_pytorch>=0.5.2 +gradio>=3.45.2 +jieba +librosa +matplotlib +numpy<=1.26.4 +pydub +pypinyin +safetensors +soundfile +tomli +torch>=2.0.0 +torchaudio>=2.0.0 +torchdiffeq +tqdm>=4.65.0 +transformers +transformers_stream_generator +vocos +wandb +x_transformers>=1.31.14 +num2words + +[eval] +faster_whisper==0.10.1 +funasr +jiwer +modelscope +zhconv +zhon diff --git a/src/f5_tts.egg-info/top_level.txt b/src/f5_tts.egg-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..afbac0d8200892b48220a9f90f3f76ca0c5379ad --- /dev/null +++ b/src/f5_tts.egg-info/top_level.txt @@ -0,0 +1,2 @@ +f5_tts +third_party diff --git a/src/f5_tts/api.py b/src/f5_tts/api.py new file mode 100644 index 0000000000000000000000000000000000000000..5468e894a103c3e4958c6d25acd1c1e15ae70448 --- /dev/null +++ b/src/f5_tts/api.py @@ -0,0 +1,151 @@ +import random +import sys +from importlib.resources import files + +import soundfile as sf +import torch +import tqdm +from cached_path import cached_path + +from f5_tts.infer.utils_infer import ( + hop_length, + infer_process, + load_model, + load_vocoder, + preprocess_ref_audio_text, + remove_silence_for_generated_wav, + save_spectrogram, + target_sample_rate, +) +from f5_tts.model import DiT, UNetT +from f5_tts.model.utils import seed_everything + + +class F5TTS: + def __init__( + self, + model_type="F5-TTS", + ckpt_file="", + vocab_file="", + ode_method="euler", + use_ema=True, + vocoder_name="vocos", + local_path=None, + device=None, + ): + # Initialize parameters + self.final_wave = None + self.target_sample_rate = target_sample_rate + self.hop_length = hop_length + self.seed = -1 + self.mel_spec_type = vocoder_name + + # Set device + self.device = device or ( + "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" + ) + + # Load models + self.load_vocoder_model(vocoder_name, local_path) + self.load_ema_model(model_type, ckpt_file, vocoder_name, vocab_file, ode_method, use_ema) + + def load_vocoder_model(self, vocoder_name, local_path): + self.vocoder = load_vocoder(vocoder_name, local_path is not None, local_path, self.device) + + def load_ema_model(self, model_type, ckpt_file, mel_spec_type, vocab_file, ode_method, use_ema): + if model_type == "F5-TTS": + if not ckpt_file: + if mel_spec_type == "vocos": + ckpt_file = str(cached_path("hf://SWivid/F5-TTS/F5TTS_Base/model_1200000.safetensors")) + elif mel_spec_type == "bigvgan": + ckpt_file = str(cached_path("hf://SWivid/F5-TTS/F5TTS_Base_bigvgan/model_1250000.pt")) + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + model_cls = DiT + elif model_type == "E2-TTS": + if not ckpt_file: + ckpt_file = str(cached_path("hf://SWivid/E2-TTS/E2TTS_Base/model_1200000.safetensors")) + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + model_cls = UNetT + else: + raise ValueError(f"Unknown model type: {model_type}") + + self.ema_model = load_model( + model_cls, model_cfg, ckpt_file, mel_spec_type, vocab_file, ode_method, use_ema, self.device + ) + + def export_wav(self, wav, file_wave, remove_silence=False): + sf.write(file_wave, wav, self.target_sample_rate) + + if remove_silence: + remove_silence_for_generated_wav(file_wave) + + def export_spectrogram(self, spect, file_spect): + save_spectrogram(spect, file_spect) + + def infer( + self, + ref_file, + ref_text, + gen_text, + show_info=print, + progress=tqdm, + target_rms=0.1, + cross_fade_duration=0.15, + sway_sampling_coef=-1, + cfg_strength=2, + nfe_step=32, + speed=1.0, + fix_duration=None, + remove_silence=False, + file_wave=None, + file_spect=None, + seed=-1, + ): + if seed == -1: + seed = random.randint(0, sys.maxsize) + seed_everything(seed) + self.seed = seed + + ref_file, ref_text = preprocess_ref_audio_text(ref_file, ref_text, device=self.device) + + wav, sr, spect = infer_process( + ref_file, + ref_text, + gen_text, + self.ema_model, + self.vocoder, + self.mel_spec_type, + show_info=show_info, + progress=progress, + target_rms=target_rms, + cross_fade_duration=cross_fade_duration, + nfe_step=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + speed=speed, + fix_duration=fix_duration, + device=self.device, + ) + + if file_wave is not None: + self.export_wav(wav, file_wave, remove_silence) + + if file_spect is not None: + self.export_spectrogram(spect, file_spect) + + return wav, sr, spect + + +if __name__ == "__main__": + f5tts = F5TTS() + + wav, sr, spect = f5tts.infer( + ref_file=str(files("f5_tts").joinpath("infer/examples/basic/basic_ref_en.wav")), + ref_text="some call me nature, others call me mother nature.", + gen_text="""I don't really care what you call me. I've been a silent spectator, watching species evolve, empires rise and fall. But always remember, I am mighty and enduring. Respect me and I'll nurture you; ignore me and you shall face the consequences.""", + file_wave=str(files("f5_tts").joinpath("../../tests/api_out.wav")), + file_spect=str(files("f5_tts").joinpath("../../tests/api_out.png")), + seed=-1, # random seed = -1 + ) + + print("seed :", f5tts.seed) diff --git a/src/f5_tts/eval/README.md b/src/f5_tts/eval/README.md new file mode 100644 index 0000000000000000000000000000000000000000..91905ee5a672b2f33c8434ab6057c8cd9f966e46 --- /dev/null +++ b/src/f5_tts/eval/README.md @@ -0,0 +1,49 @@ + +# Evaluation + +Install packages for evaluation: + +```bash +pip install -e .[eval] +``` + +## Generating Samples for Evaluation + +### Prepare Test Datasets + +1. *Seed-TTS testset*: Download from [seed-tts-eval](https://github.com/BytedanceSpeech/seed-tts-eval). +2. *LibriSpeech test-clean*: Download from [OpenSLR](http://www.openslr.org/12/). +3. Unzip the downloaded datasets and place them in the `data/` directory. +4. Update the path for *LibriSpeech test-clean* data in `src/f5_tts/eval/eval_infer_batch.py` +5. Our filtered LibriSpeech-PC 4-10s subset: `data/librispeech_pc_test_clean_cross_sentence.lst` + +### Batch Inference for Test Set + +To run batch inference for evaluations, execute the following commands: + +```bash +# batch inference for evaluations +accelerate config # if not set before +bash src/f5_tts/eval/eval_infer_batch.sh +``` + +## Objective Evaluation on Generated Results + +### Download Evaluation Model Checkpoints + +1. Chinese ASR Model: [Paraformer-zh](https://huggingface.co/funasr/paraformer-zh) +2. English ASR Model: [Faster-Whisper](https://huggingface.co/Systran/faster-whisper-large-v3) +3. WavLM Model: Download from [Google Drive](https://drive.google.com/file/d/1-aE1NfzpRCLxA4GUxX9ITI3F9LlbtEGP/view). + +Then update in the following scripts with the paths you put evaluation model ckpts to. + +### Objective Evaluation + +Update the path with your batch-inferenced results, and carry out WER / SIM evaluations: +```bash +# Evaluation for Seed-TTS test set +python src/f5_tts/eval/eval_seedtts_testset.py + +# Evaluation for LibriSpeech-PC test-clean (cross-sentence) +python src/f5_tts/eval/eval_librispeech_test_clean.py +``` \ No newline at end of file diff --git a/src/f5_tts/eval/ecapa_tdnn.py b/src/f5_tts/eval/ecapa_tdnn.py new file mode 100644 index 0000000000000000000000000000000000000000..13a5063465b59759961fdfd3468035c127784a44 --- /dev/null +++ b/src/f5_tts/eval/ecapa_tdnn.py @@ -0,0 +1,330 @@ +# just for speaker similarity evaluation, third-party code + +# From https://github.com/microsoft/UniSpeech/blob/main/downstreams/speaker_verification/models/ +# part of the code is borrowed from https://github.com/lawlict/ECAPA-TDNN + +import os +import torch +import torch.nn as nn +import torch.nn.functional as F + + +""" Res2Conv1d + BatchNorm1d + ReLU +""" + + +class Res2Conv1dReluBn(nn.Module): + """ + in_channels == out_channels == channels + """ + + def __init__(self, channels, kernel_size=1, stride=1, padding=0, dilation=1, bias=True, scale=4): + super().__init__() + assert channels % scale == 0, "{} % {} != 0".format(channels, scale) + self.scale = scale + self.width = channels // scale + self.nums = scale if scale == 1 else scale - 1 + + self.convs = [] + self.bns = [] + for i in range(self.nums): + self.convs.append(nn.Conv1d(self.width, self.width, kernel_size, stride, padding, dilation, bias=bias)) + self.bns.append(nn.BatchNorm1d(self.width)) + self.convs = nn.ModuleList(self.convs) + self.bns = nn.ModuleList(self.bns) + + def forward(self, x): + out = [] + spx = torch.split(x, self.width, 1) + for i in range(self.nums): + if i == 0: + sp = spx[i] + else: + sp = sp + spx[i] + # Order: conv -> relu -> bn + sp = self.convs[i](sp) + sp = self.bns[i](F.relu(sp)) + out.append(sp) + if self.scale != 1: + out.append(spx[self.nums]) + out = torch.cat(out, dim=1) + + return out + + +""" Conv1d + BatchNorm1d + ReLU +""" + + +class Conv1dReluBn(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size=1, stride=1, padding=0, dilation=1, bias=True): + super().__init__() + self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias) + self.bn = nn.BatchNorm1d(out_channels) + + def forward(self, x): + return self.bn(F.relu(self.conv(x))) + + +""" The SE connection of 1D case. +""" + + +class SE_Connect(nn.Module): + def __init__(self, channels, se_bottleneck_dim=128): + super().__init__() + self.linear1 = nn.Linear(channels, se_bottleneck_dim) + self.linear2 = nn.Linear(se_bottleneck_dim, channels) + + def forward(self, x): + out = x.mean(dim=2) + out = F.relu(self.linear1(out)) + out = torch.sigmoid(self.linear2(out)) + out = x * out.unsqueeze(2) + + return out + + +""" SE-Res2Block of the ECAPA-TDNN architecture. +""" + +# def SE_Res2Block(channels, kernel_size, stride, padding, dilation, scale): +# return nn.Sequential( +# Conv1dReluBn(channels, 512, kernel_size=1, stride=1, padding=0), +# Res2Conv1dReluBn(512, kernel_size, stride, padding, dilation, scale=scale), +# Conv1dReluBn(512, channels, kernel_size=1, stride=1, padding=0), +# SE_Connect(channels) +# ) + + +class SE_Res2Block(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride, padding, dilation, scale, se_bottleneck_dim): + super().__init__() + self.Conv1dReluBn1 = Conv1dReluBn(in_channels, out_channels, kernel_size=1, stride=1, padding=0) + self.Res2Conv1dReluBn = Res2Conv1dReluBn(out_channels, kernel_size, stride, padding, dilation, scale=scale) + self.Conv1dReluBn2 = Conv1dReluBn(out_channels, out_channels, kernel_size=1, stride=1, padding=0) + self.SE_Connect = SE_Connect(out_channels, se_bottleneck_dim) + + self.shortcut = None + if in_channels != out_channels: + self.shortcut = nn.Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + ) + + def forward(self, x): + residual = x + if self.shortcut: + residual = self.shortcut(x) + + x = self.Conv1dReluBn1(x) + x = self.Res2Conv1dReluBn(x) + x = self.Conv1dReluBn2(x) + x = self.SE_Connect(x) + + return x + residual + + +""" Attentive weighted mean and standard deviation pooling. +""" + + +class AttentiveStatsPool(nn.Module): + def __init__(self, in_dim, attention_channels=128, global_context_att=False): + super().__init__() + self.global_context_att = global_context_att + + # Use Conv1d with stride == 1 rather than Linear, then we don't need to transpose inputs. + if global_context_att: + self.linear1 = nn.Conv1d(in_dim * 3, attention_channels, kernel_size=1) # equals W and b in the paper + else: + self.linear1 = nn.Conv1d(in_dim, attention_channels, kernel_size=1) # equals W and b in the paper + self.linear2 = nn.Conv1d(attention_channels, in_dim, kernel_size=1) # equals V and k in the paper + + def forward(self, x): + if self.global_context_att: + context_mean = torch.mean(x, dim=-1, keepdim=True).expand_as(x) + context_std = torch.sqrt(torch.var(x, dim=-1, keepdim=True) + 1e-10).expand_as(x) + x_in = torch.cat((x, context_mean, context_std), dim=1) + else: + x_in = x + + # DON'T use ReLU here! In experiments, I find ReLU hard to converge. + alpha = torch.tanh(self.linear1(x_in)) + # alpha = F.relu(self.linear1(x_in)) + alpha = torch.softmax(self.linear2(alpha), dim=2) + mean = torch.sum(alpha * x, dim=2) + residuals = torch.sum(alpha * (x**2), dim=2) - mean**2 + std = torch.sqrt(residuals.clamp(min=1e-9)) + return torch.cat([mean, std], dim=1) + + +class ECAPA_TDNN(nn.Module): + def __init__( + self, + feat_dim=80, + channels=512, + emb_dim=192, + global_context_att=False, + feat_type="wavlm_large", + sr=16000, + feature_selection="hidden_states", + update_extract=False, + config_path=None, + ): + super().__init__() + + self.feat_type = feat_type + self.feature_selection = feature_selection + self.update_extract = update_extract + self.sr = sr + + torch.hub._validate_not_a_forked_repo = lambda a, b, c: True + try: + local_s3prl_path = os.path.expanduser("~/.cache/torch/hub/s3prl_s3prl_main") + self.feature_extract = torch.hub.load(local_s3prl_path, feat_type, source="local", config_path=config_path) + except: # noqa: E722 + self.feature_extract = torch.hub.load("s3prl/s3prl", feat_type) + + if len(self.feature_extract.model.encoder.layers) == 24 and hasattr( + self.feature_extract.model.encoder.layers[23].self_attn, "fp32_attention" + ): + self.feature_extract.model.encoder.layers[23].self_attn.fp32_attention = False + if len(self.feature_extract.model.encoder.layers) == 24 and hasattr( + self.feature_extract.model.encoder.layers[11].self_attn, "fp32_attention" + ): + self.feature_extract.model.encoder.layers[11].self_attn.fp32_attention = False + + self.feat_num = self.get_feat_num() + self.feature_weight = nn.Parameter(torch.zeros(self.feat_num)) + + if feat_type != "fbank" and feat_type != "mfcc": + freeze_list = ["final_proj", "label_embs_concat", "mask_emb", "project_q", "quantizer"] + for name, param in self.feature_extract.named_parameters(): + for freeze_val in freeze_list: + if freeze_val in name: + param.requires_grad = False + break + + if not self.update_extract: + for param in self.feature_extract.parameters(): + param.requires_grad = False + + self.instance_norm = nn.InstanceNorm1d(feat_dim) + # self.channels = [channels] * 4 + [channels * 3] + self.channels = [channels] * 4 + [1536] + + self.layer1 = Conv1dReluBn(feat_dim, self.channels[0], kernel_size=5, padding=2) + self.layer2 = SE_Res2Block( + self.channels[0], + self.channels[1], + kernel_size=3, + stride=1, + padding=2, + dilation=2, + scale=8, + se_bottleneck_dim=128, + ) + self.layer3 = SE_Res2Block( + self.channels[1], + self.channels[2], + kernel_size=3, + stride=1, + padding=3, + dilation=3, + scale=8, + se_bottleneck_dim=128, + ) + self.layer4 = SE_Res2Block( + self.channels[2], + self.channels[3], + kernel_size=3, + stride=1, + padding=4, + dilation=4, + scale=8, + se_bottleneck_dim=128, + ) + + # self.conv = nn.Conv1d(self.channels[-1], self.channels[-1], kernel_size=1) + cat_channels = channels * 3 + self.conv = nn.Conv1d(cat_channels, self.channels[-1], kernel_size=1) + self.pooling = AttentiveStatsPool( + self.channels[-1], attention_channels=128, global_context_att=global_context_att + ) + self.bn = nn.BatchNorm1d(self.channels[-1] * 2) + self.linear = nn.Linear(self.channels[-1] * 2, emb_dim) + + def get_feat_num(self): + self.feature_extract.eval() + wav = [torch.randn(self.sr).to(next(self.feature_extract.parameters()).device)] + with torch.no_grad(): + features = self.feature_extract(wav) + select_feature = features[self.feature_selection] + if isinstance(select_feature, (list, tuple)): + return len(select_feature) + else: + return 1 + + def get_feat(self, x): + if self.update_extract: + x = self.feature_extract([sample for sample in x]) + else: + with torch.no_grad(): + if self.feat_type == "fbank" or self.feat_type == "mfcc": + x = self.feature_extract(x) + 1e-6 # B x feat_dim x time_len + else: + x = self.feature_extract([sample for sample in x]) + + if self.feat_type == "fbank": + x = x.log() + + if self.feat_type != "fbank" and self.feat_type != "mfcc": + x = x[self.feature_selection] + if isinstance(x, (list, tuple)): + x = torch.stack(x, dim=0) + else: + x = x.unsqueeze(0) + norm_weights = F.softmax(self.feature_weight, dim=-1).unsqueeze(-1).unsqueeze(-1).unsqueeze(-1) + x = (norm_weights * x).sum(dim=0) + x = torch.transpose(x, 1, 2) + 1e-6 + + x = self.instance_norm(x) + return x + + def forward(self, x): + x = self.get_feat(x) + + out1 = self.layer1(x) + out2 = self.layer2(out1) + out3 = self.layer3(out2) + out4 = self.layer4(out3) + + out = torch.cat([out2, out3, out4], dim=1) + out = F.relu(self.conv(out)) + out = self.bn(self.pooling(out)) + out = self.linear(out) + + return out + + +def ECAPA_TDNN_SMALL( + feat_dim, + emb_dim=256, + feat_type="wavlm_large", + sr=16000, + feature_selection="hidden_states", + update_extract=False, + config_path=None, +): + return ECAPA_TDNN( + feat_dim=feat_dim, + channels=512, + emb_dim=emb_dim, + feat_type=feat_type, + sr=sr, + feature_selection=feature_selection, + update_extract=update_extract, + config_path=config_path, + ) diff --git a/src/f5_tts/eval/eval_infer_batch.py b/src/f5_tts/eval/eval_infer_batch.py new file mode 100644 index 0000000000000000000000000000000000000000..814eabb8d487ebd33fccf85d47d81e40ef14ee1a --- /dev/null +++ b/src/f5_tts/eval/eval_infer_batch.py @@ -0,0 +1,207 @@ +import os +import sys + +sys.path.append(os.getcwd()) + +import argparse +import time +from importlib.resources import files + +import torch +import torchaudio +from accelerate import Accelerator +from tqdm import tqdm + +from f5_tts.eval.utils_eval import ( + get_inference_prompt, + get_librispeech_test_clean_metainfo, + get_seedtts_testset_metainfo, +) +from f5_tts.infer.utils_infer import load_checkpoint, load_vocoder +from f5_tts.model import CFM, DiT, UNetT +from f5_tts.model.utils import get_tokenizer + +accelerator = Accelerator() +device = f"cuda:{accelerator.process_index}" + + +# --------------------- Dataset Settings -------------------- # + +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 +win_length = 1024 +n_fft = 1024 +target_rms = 0.1 + + +tokenizer = "pinyin" +rel_path = str(files("f5_tts").joinpath("../../")) + + +def main(): + # ---------------------- infer setting ---------------------- # + + parser = argparse.ArgumentParser(description="batch inference") + + parser.add_argument("-s", "--seed", default=None, type=int) + parser.add_argument("-d", "--dataset", default="Emilia_ZH_EN") + parser.add_argument("-n", "--expname", required=True) + parser.add_argument("-c", "--ckptstep", default=1200000, type=int) + parser.add_argument("-m", "--mel_spec_type", default="vocos", type=str, choices=["bigvgan", "vocos"]) + + parser.add_argument("-nfe", "--nfestep", default=32, type=int) + parser.add_argument("-o", "--odemethod", default="euler") + parser.add_argument("-ss", "--swaysampling", default=-1, type=float) + + parser.add_argument("-t", "--testset", required=True) + + args = parser.parse_args() + + seed = args.seed + dataset_name = args.dataset + exp_name = args.expname + ckpt_step = args.ckptstep + ckpt_path = rel_path + f"/ckpts/{exp_name}/model_{ckpt_step}.pt" + mel_spec_type = args.mel_spec_type + + nfe_step = args.nfestep + ode_method = args.odemethod + sway_sampling_coef = args.swaysampling + + testset = args.testset + + infer_batch_size = 1 # max frames. 1 for ddp single inference (recommended) + cfg_strength = 2.0 + speed = 1.0 + use_truth_duration = False + no_ref_audio = False + + if exp_name == "F5TTS_Base": + model_cls = DiT + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + + elif exp_name == "E2TTS_Base": + model_cls = UNetT + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + + if testset == "ls_pc_test_clean": + metalst = rel_path + "/data/librispeech_pc_test_clean_cross_sentence.lst" + librispeech_test_clean_path = "/LibriSpeech/test-clean" # test-clean path + metainfo = get_librispeech_test_clean_metainfo(metalst, librispeech_test_clean_path) + + elif testset == "seedtts_test_zh": + metalst = rel_path + "/data/seedtts_testset/zh/meta.lst" + metainfo = get_seedtts_testset_metainfo(metalst) + + elif testset == "seedtts_test_en": + metalst = rel_path + "/data/seedtts_testset/en/meta.lst" + metainfo = get_seedtts_testset_metainfo(metalst) + + # path to save genereted wavs + output_dir = ( + f"{rel_path}/" + f"results/{exp_name}_{ckpt_step}/{testset}/" + f"seed{seed}_{ode_method}_nfe{nfe_step}_{mel_spec_type}" + f"{f'_ss{sway_sampling_coef}' if sway_sampling_coef else ''}" + f"_cfg{cfg_strength}_speed{speed}" + f"{'_gt-dur' if use_truth_duration else ''}" + f"{'_no-ref-audio' if no_ref_audio else ''}" + ) + + # -------------------------------------------------# + + use_ema = True + + prompts_all = get_inference_prompt( + metainfo, + speed=speed, + tokenizer=tokenizer, + target_sample_rate=target_sample_rate, + n_mel_channels=n_mel_channels, + hop_length=hop_length, + mel_spec_type=mel_spec_type, + target_rms=target_rms, + use_truth_duration=use_truth_duration, + infer_batch_size=infer_batch_size, + ) + + # Vocoder model + local = False + if mel_spec_type == "vocos": + vocoder_local_path = "../checkpoints/charactr/vocos-mel-24khz" + elif mel_spec_type == "bigvgan": + vocoder_local_path = "../checkpoints/bigvgan_v2_24khz_100band_256x" + vocoder = load_vocoder(vocoder_name=mel_spec_type, is_local=local, local_path=vocoder_local_path) + + # Tokenizer + vocab_char_map, vocab_size = get_tokenizer(dataset_name, tokenizer) + + # Model + model = CFM( + transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels), + mel_spec_kwargs=dict( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ), + odeint_kwargs=dict( + method=ode_method, + ), + vocab_char_map=vocab_char_map, + ).to(device) + + dtype = torch.float32 if mel_spec_type == "bigvgan" else None + model = load_checkpoint(model, ckpt_path, device, dtype=dtype, use_ema=use_ema) + + if not os.path.exists(output_dir) and accelerator.is_main_process: + os.makedirs(output_dir) + + # start batch inference + accelerator.wait_for_everyone() + start = time.time() + + with accelerator.split_between_processes(prompts_all) as prompts: + for prompt in tqdm(prompts, disable=not accelerator.is_local_main_process): + utts, ref_rms_list, ref_mels, ref_mel_lens, total_mel_lens, final_text_list = prompt + ref_mels = ref_mels.to(device) + ref_mel_lens = torch.tensor(ref_mel_lens, dtype=torch.long).to(device) + total_mel_lens = torch.tensor(total_mel_lens, dtype=torch.long).to(device) + + # Inference + with torch.inference_mode(): + generated, _ = model.sample( + cond=ref_mels, + text=final_text_list, + duration=total_mel_lens, + lens=ref_mel_lens, + steps=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + no_ref_audio=no_ref_audio, + seed=seed, + ) + # Final result + for i, gen in enumerate(generated): + gen = gen[ref_mel_lens[i] : total_mel_lens[i], :].unsqueeze(0) + gen_mel_spec = gen.permute(0, 2, 1) + if mel_spec_type == "vocos": + generated_wave = vocoder.decode(gen_mel_spec) + elif mel_spec_type == "bigvgan": + generated_wave = vocoder(gen_mel_spec) + + if ref_rms_list[i] < target_rms: + generated_wave = generated_wave * ref_rms_list[i] / target_rms + torchaudio.save(f"{output_dir}/{utts[i]}.wav", generated_wave.squeeze(0).cpu(), target_sample_rate) + + accelerator.wait_for_everyone() + if accelerator.is_main_process: + timediff = time.time() - start + print(f"Done batch inference in {timediff / 60 :.2f} minutes.") + + +if __name__ == "__main__": + main() diff --git a/src/f5_tts/eval/eval_infer_batch.sh b/src/f5_tts/eval/eval_infer_batch.sh new file mode 100644 index 0000000000000000000000000000000000000000..77a9ec65d9e355e23d17ebdcd9efc3cbb08b7f1a --- /dev/null +++ b/src/f5_tts/eval/eval_infer_batch.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# e.g. F5-TTS, 16 NFE +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "F5TTS_Base" -t "seedtts_test_zh" -nfe 16 +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "F5TTS_Base" -t "seedtts_test_en" -nfe 16 +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "F5TTS_Base" -t "ls_pc_test_clean" -nfe 16 + +# e.g. Vanilla E2 TTS, 32 NFE +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "E2TTS_Base" -t "seedtts_test_zh" -o "midpoint" -ss 0 +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "E2TTS_Base" -t "seedtts_test_en" -o "midpoint" -ss 0 +accelerate launch src/f5_tts/eval/eval_infer_batch.py -s 0 -n "E2TTS_Base" -t "ls_pc_test_clean" -o "midpoint" -ss 0 + +# etc. diff --git a/src/f5_tts/eval/eval_librispeech_test_clean.py b/src/f5_tts/eval/eval_librispeech_test_clean.py new file mode 100644 index 0000000000000000000000000000000000000000..006222e181bfe4e98ba21c14b8c04175f2e3a191 --- /dev/null +++ b/src/f5_tts/eval/eval_librispeech_test_clean.py @@ -0,0 +1,73 @@ +# Evaluate with Librispeech test-clean, ~3s prompt to generate 4-10s audio (the way of valle/voicebox evaluation) + +import sys +import os + +sys.path.append(os.getcwd()) + +import multiprocessing as mp +from importlib.resources import files + +import numpy as np + +from f5_tts.eval.utils_eval import ( + get_librispeech_test, + run_asr_wer, + run_sim, +) + +rel_path = str(files("f5_tts").joinpath("../../")) + + +eval_task = "wer" # sim | wer +lang = "en" +metalst = rel_path + "/data/librispeech_pc_test_clean_cross_sentence.lst" +librispeech_test_clean_path = "/LibriSpeech/test-clean" # test-clean path +gen_wav_dir = "PATH_TO_GENERATED" # generated wavs + +gpus = [0, 1, 2, 3, 4, 5, 6, 7] +test_set = get_librispeech_test(metalst, gen_wav_dir, gpus, librispeech_test_clean_path) + +## In LibriSpeech, some speakers utilized varying voice characteristics for different characters in the book, +## leading to a low similarity for the ground truth in some cases. +# test_set = get_librispeech_test(metalst, gen_wav_dir, gpus, librispeech_test_clean_path, eval_ground_truth = True) # eval ground truth + +local = False +if local: # use local custom checkpoint dir + asr_ckpt_dir = "../checkpoints/Systran/faster-whisper-large-v3" +else: + asr_ckpt_dir = "" # auto download to cache dir + +wavlm_ckpt_dir = "../checkpoints/UniSpeech/wavlm_large_finetune.pth" + + +# --------------------------- WER --------------------------- + +if eval_task == "wer": + wers = [] + + with mp.Pool(processes=len(gpus)) as pool: + args = [(rank, lang, sub_test_set, asr_ckpt_dir) for (rank, sub_test_set) in test_set] + results = pool.map(run_asr_wer, args) + for wers_ in results: + wers.extend(wers_) + + wer = round(np.mean(wers) * 100, 3) + print(f"\nTotal {len(wers)} samples") + print(f"WER : {wer}%") + + +# --------------------------- SIM --------------------------- + +if eval_task == "sim": + sim_list = [] + + with mp.Pool(processes=len(gpus)) as pool: + args = [(rank, sub_test_set, wavlm_ckpt_dir) for (rank, sub_test_set) in test_set] + results = pool.map(run_sim, args) + for sim_ in results: + sim_list.extend(sim_) + + sim = round(sum(sim_list) / len(sim_list), 3) + print(f"\nTotal {len(sim_list)} samples") + print(f"SIM : {sim}") diff --git a/src/f5_tts/eval/eval_seedtts_testset.py b/src/f5_tts/eval/eval_seedtts_testset.py new file mode 100644 index 0000000000000000000000000000000000000000..03d1f327b14bba06dd6c99ec857bde607d572ef6 --- /dev/null +++ b/src/f5_tts/eval/eval_seedtts_testset.py @@ -0,0 +1,75 @@ +# Evaluate with Seed-TTS testset + +import sys +import os + +sys.path.append(os.getcwd()) + +import multiprocessing as mp +from importlib.resources import files + +import numpy as np + +from f5_tts.eval.utils_eval import ( + get_seed_tts_test, + run_asr_wer, + run_sim, +) + +rel_path = str(files("f5_tts").joinpath("../../")) + + +eval_task = "wer" # sim | wer +lang = "zh" # zh | en +metalst = rel_path + f"/data/seedtts_testset/{lang}/meta.lst" # seed-tts testset +# gen_wav_dir = rel_path + f"/data/seedtts_testset/{lang}/wavs" # ground truth wavs +gen_wav_dir = "PATH_TO_GENERATED" # generated wavs + + +# NOTE. paraformer-zh result will be slightly different according to the number of gpus, cuz batchsize is different +# zh 1.254 seems a result of 4 workers wer_seed_tts +gpus = [0, 1, 2, 3, 4, 5, 6, 7] +test_set = get_seed_tts_test(metalst, gen_wav_dir, gpus) + +local = False +if local: # use local custom checkpoint dir + if lang == "zh": + asr_ckpt_dir = "../checkpoints/funasr" # paraformer-zh dir under funasr + elif lang == "en": + asr_ckpt_dir = "../checkpoints/Systran/faster-whisper-large-v3" +else: + asr_ckpt_dir = "" # auto download to cache dir + +wavlm_ckpt_dir = "../checkpoints/UniSpeech/wavlm_large_finetune.pth" + + +# --------------------------- WER --------------------------- + +if eval_task == "wer": + wers = [] + + with mp.Pool(processes=len(gpus)) as pool: + args = [(rank, lang, sub_test_set, asr_ckpt_dir) for (rank, sub_test_set) in test_set] + results = pool.map(run_asr_wer, args) + for wers_ in results: + wers.extend(wers_) + + wer = round(np.mean(wers) * 100, 3) + print(f"\nTotal {len(wers)} samples") + print(f"WER : {wer}%") + + +# --------------------------- SIM --------------------------- + +if eval_task == "sim": + sim_list = [] + + with mp.Pool(processes=len(gpus)) as pool: + args = [(rank, sub_test_set, wavlm_ckpt_dir) for (rank, sub_test_set) in test_set] + results = pool.map(run_sim, args) + for sim_ in results: + sim_list.extend(sim_) + + sim = round(sum(sim_list) / len(sim_list), 3) + print(f"\nTotal {len(sim_list)} samples") + print(f"SIM : {sim}") diff --git a/src/f5_tts/eval/utils_eval.py b/src/f5_tts/eval/utils_eval.py new file mode 100644 index 0000000000000000000000000000000000000000..3ebfe41886f10b2a597ab098543ccc3ea672ab3a --- /dev/null +++ b/src/f5_tts/eval/utils_eval.py @@ -0,0 +1,405 @@ +import math +import os +import random +import string + +import torch +import torch.nn.functional as F +import torchaudio +from tqdm import tqdm + +from f5_tts.eval.ecapa_tdnn import ECAPA_TDNN_SMALL +from f5_tts.model.modules import MelSpec +from f5_tts.model.utils import convert_char_to_pinyin + + +# seedtts testset metainfo: utt, prompt_text, prompt_wav, gt_text, gt_wav +def get_seedtts_testset_metainfo(metalst): + f = open(metalst) + lines = f.readlines() + f.close() + metainfo = [] + for line in lines: + if len(line.strip().split("|")) == 5: + utt, prompt_text, prompt_wav, gt_text, gt_wav = line.strip().split("|") + elif len(line.strip().split("|")) == 4: + utt, prompt_text, prompt_wav, gt_text = line.strip().split("|") + gt_wav = os.path.join(os.path.dirname(metalst), "wavs", utt + ".wav") + if not os.path.isabs(prompt_wav): + prompt_wav = os.path.join(os.path.dirname(metalst), prompt_wav) + metainfo.append((utt, prompt_text, prompt_wav, gt_text, gt_wav)) + return metainfo + + +# librispeech test-clean metainfo: gen_utt, ref_txt, ref_wav, gen_txt, gen_wav +def get_librispeech_test_clean_metainfo(metalst, librispeech_test_clean_path): + f = open(metalst) + lines = f.readlines() + f.close() + metainfo = [] + for line in lines: + ref_utt, ref_dur, ref_txt, gen_utt, gen_dur, gen_txt = line.strip().split("\t") + + # ref_txt = ref_txt[0] + ref_txt[1:].lower() + '.' # if use librispeech test-clean (no-pc) + ref_spk_id, ref_chaptr_id, _ = ref_utt.split("-") + ref_wav = os.path.join(librispeech_test_clean_path, ref_spk_id, ref_chaptr_id, ref_utt + ".flac") + + # gen_txt = gen_txt[0] + gen_txt[1:].lower() + '.' # if use librispeech test-clean (no-pc) + gen_spk_id, gen_chaptr_id, _ = gen_utt.split("-") + gen_wav = os.path.join(librispeech_test_clean_path, gen_spk_id, gen_chaptr_id, gen_utt + ".flac") + + metainfo.append((gen_utt, ref_txt, ref_wav, " " + gen_txt, gen_wav)) + + return metainfo + + +# padded to max length mel batch +def padded_mel_batch(ref_mels): + max_mel_length = torch.LongTensor([mel.shape[-1] for mel in ref_mels]).amax() + padded_ref_mels = [] + for mel in ref_mels: + padded_ref_mel = F.pad(mel, (0, max_mel_length - mel.shape[-1]), value=0) + padded_ref_mels.append(padded_ref_mel) + padded_ref_mels = torch.stack(padded_ref_mels) + padded_ref_mels = padded_ref_mels.permute(0, 2, 1) + return padded_ref_mels + + +# get prompts from metainfo containing: utt, prompt_text, prompt_wav, gt_text, gt_wav + + +def get_inference_prompt( + metainfo, + speed=1.0, + tokenizer="pinyin", + polyphone=True, + target_sample_rate=24000, + n_fft=1024, + win_length=1024, + n_mel_channels=100, + hop_length=256, + mel_spec_type="vocos", + target_rms=0.1, + use_truth_duration=False, + infer_batch_size=1, + num_buckets=200, + min_secs=3, + max_secs=40, +): + prompts_all = [] + + min_tokens = min_secs * target_sample_rate // hop_length + max_tokens = max_secs * target_sample_rate // hop_length + + batch_accum = [0] * num_buckets + utts, ref_rms_list, ref_mels, ref_mel_lens, total_mel_lens, final_text_list = ( + [[] for _ in range(num_buckets)] for _ in range(6) + ) + + mel_spectrogram = MelSpec( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ) + + for utt, prompt_text, prompt_wav, gt_text, gt_wav in tqdm(metainfo, desc="Processing prompts..."): + # Audio + ref_audio, ref_sr = torchaudio.load(prompt_wav) + ref_rms = torch.sqrt(torch.mean(torch.square(ref_audio))) + if ref_rms < target_rms: + ref_audio = ref_audio * target_rms / ref_rms + assert ref_audio.shape[-1] > 5000, f"Empty prompt wav: {prompt_wav}, or torchaudio backend issue." + if ref_sr != target_sample_rate: + resampler = torchaudio.transforms.Resample(ref_sr, target_sample_rate) + ref_audio = resampler(ref_audio) + + # Text + if len(prompt_text[-1].encode("utf-8")) == 1: + prompt_text = prompt_text + " " + text = [prompt_text + gt_text] + if tokenizer == "pinyin": + text_list = convert_char_to_pinyin(text, polyphone=polyphone) + else: + text_list = text + + # Duration, mel frame length + ref_mel_len = ref_audio.shape[-1] // hop_length + if use_truth_duration: + gt_audio, gt_sr = torchaudio.load(gt_wav) + if gt_sr != target_sample_rate: + resampler = torchaudio.transforms.Resample(gt_sr, target_sample_rate) + gt_audio = resampler(gt_audio) + total_mel_len = ref_mel_len + int(gt_audio.shape[-1] / hop_length / speed) + + # # test vocoder resynthesis + # ref_audio = gt_audio + else: + ref_text_len = len(prompt_text.encode("utf-8")) + gen_text_len = len(gt_text.encode("utf-8")) + total_mel_len = ref_mel_len + int(ref_mel_len / ref_text_len * gen_text_len / speed) + + # to mel spectrogram + ref_mel = mel_spectrogram(ref_audio) + ref_mel = ref_mel.squeeze(0) + + # deal with batch + assert infer_batch_size > 0, "infer_batch_size should be greater than 0." + assert ( + min_tokens <= total_mel_len <= max_tokens + ), f"Audio {utt} has duration {total_mel_len*hop_length//target_sample_rate}s out of range [{min_secs}, {max_secs}]." + bucket_i = math.floor((total_mel_len - min_tokens) / (max_tokens - min_tokens + 1) * num_buckets) + + utts[bucket_i].append(utt) + ref_rms_list[bucket_i].append(ref_rms) + ref_mels[bucket_i].append(ref_mel) + ref_mel_lens[bucket_i].append(ref_mel_len) + total_mel_lens[bucket_i].append(total_mel_len) + final_text_list[bucket_i].extend(text_list) + + batch_accum[bucket_i] += total_mel_len + + if batch_accum[bucket_i] >= infer_batch_size: + # print(f"\n{len(ref_mels[bucket_i][0][0])}\n{ref_mel_lens[bucket_i]}\n{total_mel_lens[bucket_i]}") + prompts_all.append( + ( + utts[bucket_i], + ref_rms_list[bucket_i], + padded_mel_batch(ref_mels[bucket_i]), + ref_mel_lens[bucket_i], + total_mel_lens[bucket_i], + final_text_list[bucket_i], + ) + ) + batch_accum[bucket_i] = 0 + ( + utts[bucket_i], + ref_rms_list[bucket_i], + ref_mels[bucket_i], + ref_mel_lens[bucket_i], + total_mel_lens[bucket_i], + final_text_list[bucket_i], + ) = [], [], [], [], [], [] + + # add residual + for bucket_i, bucket_frames in enumerate(batch_accum): + if bucket_frames > 0: + prompts_all.append( + ( + utts[bucket_i], + ref_rms_list[bucket_i], + padded_mel_batch(ref_mels[bucket_i]), + ref_mel_lens[bucket_i], + total_mel_lens[bucket_i], + final_text_list[bucket_i], + ) + ) + # not only leave easy work for last workers + random.seed(666) + random.shuffle(prompts_all) + + return prompts_all + + +# get wav_res_ref_text of seed-tts test metalst +# https://github.com/BytedanceSpeech/seed-tts-eval + + +def get_seed_tts_test(metalst, gen_wav_dir, gpus): + f = open(metalst) + lines = f.readlines() + f.close() + + test_set_ = [] + for line in tqdm(lines): + if len(line.strip().split("|")) == 5: + utt, prompt_text, prompt_wav, gt_text, gt_wav = line.strip().split("|") + elif len(line.strip().split("|")) == 4: + utt, prompt_text, prompt_wav, gt_text = line.strip().split("|") + + if not os.path.exists(os.path.join(gen_wav_dir, utt + ".wav")): + continue + gen_wav = os.path.join(gen_wav_dir, utt + ".wav") + if not os.path.isabs(prompt_wav): + prompt_wav = os.path.join(os.path.dirname(metalst), prompt_wav) + + test_set_.append((gen_wav, prompt_wav, gt_text)) + + num_jobs = len(gpus) + if num_jobs == 1: + return [(gpus[0], test_set_)] + + wav_per_job = len(test_set_) // num_jobs + 1 + test_set = [] + for i in range(num_jobs): + test_set.append((gpus[i], test_set_[i * wav_per_job : (i + 1) * wav_per_job])) + + return test_set + + +# get librispeech test-clean cross sentence test + + +def get_librispeech_test(metalst, gen_wav_dir, gpus, librispeech_test_clean_path, eval_ground_truth=False): + f = open(metalst) + lines = f.readlines() + f.close() + + test_set_ = [] + for line in tqdm(lines): + ref_utt, ref_dur, ref_txt, gen_utt, gen_dur, gen_txt = line.strip().split("\t") + + if eval_ground_truth: + gen_spk_id, gen_chaptr_id, _ = gen_utt.split("-") + gen_wav = os.path.join(librispeech_test_clean_path, gen_spk_id, gen_chaptr_id, gen_utt + ".flac") + else: + if not os.path.exists(os.path.join(gen_wav_dir, gen_utt + ".wav")): + raise FileNotFoundError(f"Generated wav not found: {gen_utt}") + gen_wav = os.path.join(gen_wav_dir, gen_utt + ".wav") + + ref_spk_id, ref_chaptr_id, _ = ref_utt.split("-") + ref_wav = os.path.join(librispeech_test_clean_path, ref_spk_id, ref_chaptr_id, ref_utt + ".flac") + + test_set_.append((gen_wav, ref_wav, gen_txt)) + + num_jobs = len(gpus) + if num_jobs == 1: + return [(gpus[0], test_set_)] + + wav_per_job = len(test_set_) // num_jobs + 1 + test_set = [] + for i in range(num_jobs): + test_set.append((gpus[i], test_set_[i * wav_per_job : (i + 1) * wav_per_job])) + + return test_set + + +# load asr model + + +def load_asr_model(lang, ckpt_dir=""): + if lang == "zh": + from funasr import AutoModel + + model = AutoModel( + model=os.path.join(ckpt_dir, "paraformer-zh"), + # vad_model = os.path.join(ckpt_dir, "fsmn-vad"), + # punc_model = os.path.join(ckpt_dir, "ct-punc"), + # spk_model = os.path.join(ckpt_dir, "cam++"), + disable_update=True, + ) # following seed-tts setting + elif lang == "en": + from faster_whisper import WhisperModel + + model_size = "large-v3" if ckpt_dir == "" else ckpt_dir + model = WhisperModel(model_size, device="cuda", compute_type="float16") + return model + + +# WER Evaluation, the way Seed-TTS does + + +def run_asr_wer(args): + rank, lang, test_set, ckpt_dir = args + + if lang == "zh": + import zhconv + + torch.cuda.set_device(rank) + elif lang == "en": + os.environ["CUDA_VISIBLE_DEVICES"] = str(rank) + else: + raise NotImplementedError( + "lang support only 'zh' (funasr paraformer-zh), 'en' (faster-whisper-large-v3), for now." + ) + + asr_model = load_asr_model(lang, ckpt_dir=ckpt_dir) + + from zhon.hanzi import punctuation + + punctuation_all = punctuation + string.punctuation + wers = [] + + from jiwer import compute_measures + + for gen_wav, prompt_wav, truth in tqdm(test_set): + if lang == "zh": + res = asr_model.generate(input=gen_wav, batch_size_s=300, disable_pbar=True) + hypo = res[0]["text"] + hypo = zhconv.convert(hypo, "zh-cn") + elif lang == "en": + segments, _ = asr_model.transcribe(gen_wav, beam_size=5, language="en") + hypo = "" + for segment in segments: + hypo = hypo + " " + segment.text + + # raw_truth = truth + # raw_hypo = hypo + + for x in punctuation_all: + truth = truth.replace(x, "") + hypo = hypo.replace(x, "") + + truth = truth.replace(" ", " ") + hypo = hypo.replace(" ", " ") + + if lang == "zh": + truth = " ".join([x for x in truth]) + hypo = " ".join([x for x in hypo]) + elif lang == "en": + truth = truth.lower() + hypo = hypo.lower() + + measures = compute_measures(truth, hypo) + wer = measures["wer"] + + # ref_list = truth.split(" ") + # subs = measures["substitutions"] / len(ref_list) + # dele = measures["deletions"] / len(ref_list) + # inse = measures["insertions"] / len(ref_list) + + wers.append(wer) + + return wers + + +# SIM Evaluation + + +def run_sim(args): + rank, test_set, ckpt_dir = args + device = f"cuda:{rank}" + + model = ECAPA_TDNN_SMALL(feat_dim=1024, feat_type="wavlm_large", config_path=None) + state_dict = torch.load(ckpt_dir, weights_only=True, map_location=lambda storage, loc: storage) + model.load_state_dict(state_dict["model"], strict=False) + + use_gpu = True if torch.cuda.is_available() else False + if use_gpu: + model = model.cuda(device) + model.eval() + + sim_list = [] + for wav1, wav2, truth in tqdm(test_set): + wav1, sr1 = torchaudio.load(wav1) + wav2, sr2 = torchaudio.load(wav2) + + resample1 = torchaudio.transforms.Resample(orig_freq=sr1, new_freq=16000) + resample2 = torchaudio.transforms.Resample(orig_freq=sr2, new_freq=16000) + wav1 = resample1(wav1) + wav2 = resample2(wav2) + + if use_gpu: + wav1 = wav1.cuda(device) + wav2 = wav2.cuda(device) + with torch.no_grad(): + emb1 = model(wav1) + emb2 = model(wav2) + + sim = F.cosine_similarity(emb1, emb2)[0].item() + # print(f"VSim score between two audios: {sim:.4f} (-1.0, 1.0).") + sim_list.append(sim) + + return sim_list diff --git a/src/f5_tts/infer/README.md b/src/f5_tts/infer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dae1eb7eb5c391358497339656d0e317b434be5e --- /dev/null +++ b/src/f5_tts/infer/README.md @@ -0,0 +1,189 @@ +# Inference + +The pretrained model checkpoints can be reached at [🤗 Hugging Face](https://huggingface.co/SWivid/F5-TTS) and [🤖 Model Scope](https://www.modelscope.cn/models/SWivid/F5-TTS_Emilia-ZH-EN), or will be automatically downloaded when running inference scripts. + +Currently support **30s for a single** generation, which is the **total length** including both prompt and output audio. However, you can provide `infer_cli` and `infer_gradio` with longer text, will automatically do chunk generation. Long reference audio will be **clip short to ~15s**. + +To avoid possible inference failures, make sure you have seen through the following instructions. + +- Use reference audio <15s and leave some silence (e.g. 1s) at the end. Otherwise there is a risk of truncating in the middle of word, leading to suboptimal generation. +- Uppercased letters will be uttered letter by letter, so use lowercased letters for normal words. +- Add some spaces (blank: " ") or punctuations (e.g. "," ".") to explicitly introduce some pauses. +- Preprocess numbers to Chinese letters if you want to have them read in Chinese, otherwise in English. + + +## Gradio App + +Currently supported features: + +- Basic TTS with Chunk Inference +- Multi-Style / Multi-Speaker Generation +- Voice Chat powered by Qwen2.5-3B-Instruct + +The cli command `f5-tts_infer-gradio` equals to `python src/f5_tts/infer/infer_gradio.py`, which launches a Gradio APP (web interface) for inference. + +The script will load model checkpoints from Huggingface. You can also manually download files and update the path to `load_model()` in `infer_gradio.py`. Currently only load TTS models first, will load ASR model to do transcription if `ref_text` not provided, will load LLM model if use Voice Chat. + +Could also be used as a component for larger application. +```python +import gradio as gr +from f5_tts.infer.infer_gradio import app + +with gr.Blocks() as main_app: + gr.Markdown("# This is an example of using F5-TTS within a bigger Gradio app") + + # ... other Gradio components + + app.render() + +main_app.launch() +``` + + +## CLI Inference + +The cli command `f5-tts_infer-cli` equals to `python src/f5_tts/infer/infer_cli.py`, which is a command line tool for inference. + +The script will load model checkpoints from Huggingface. You can also manually download files and use `--ckpt_file` to specify the model you want to load, or directly update in `infer_cli.py`. + +For change vocab.txt use `--vocab_file` to provide your `vocab.txt` file. + +Basically you can inference with flags: +```bash +# Leave --ref_text "" will have ASR model transcribe (extra GPU memory usage) +f5-tts_infer-cli \ +--model "F5-TTS" \ +--ref_audio "ref_audio.wav" \ +--ref_text "The content, subtitle or transcription of reference audio." \ +--gen_text "Some text you want TTS model generate for you." + +# Choose Vocoder +f5-tts_infer-cli --vocoder_name bigvgan --load_vocoder_from_local --ckpt_file +f5-tts_infer-cli --vocoder_name vocos --load_vocoder_from_local --ckpt_file +``` + +And a `.toml` file would help with more flexible usage. + +```bash +f5-tts_infer-cli -c custom.toml +``` + +For example, you can use `.toml` to pass in variables, refer to `src/f5_tts/infer/examples/basic/basic.toml`: + +```toml +# F5-TTS | E2-TTS +model = "F5-TTS" +ref_audio = "infer/examples/basic/basic_ref_en.wav" +# If an empty "", transcribes the reference audio automatically. +ref_text = "Some call me nature, others call me mother nature." +gen_text = "I don't really care what you call me. I've been a silent spectator, watching species evolve, empires rise and fall. But always remember, I am mighty and enduring." +# File with text to generate. Ignores the text above. +gen_file = "" +remove_silence = false +output_dir = "tests" +``` + +You can also leverage `.toml` file to do multi-style generation, refer to `src/f5_tts/infer/examples/multi/story.toml`. + +```toml +# F5-TTS | E2-TTS +model = "F5-TTS" +ref_audio = "infer/examples/multi/main.flac" +# If an empty "", transcribes the reference audio automatically. +ref_text = "" +gen_text = "" +# File with text to generate. Ignores the text above. +gen_file = "infer/examples/multi/story.txt" +remove_silence = true +output_dir = "tests" + +[voices.town] +ref_audio = "infer/examples/multi/town.flac" +ref_text = "" + +[voices.country] +ref_audio = "infer/examples/multi/country.flac" +ref_text = "" +``` +You should mark the voice with `[main]` `[town]` `[country]` whenever you want to change voice, refer to `src/f5_tts/infer/examples/multi/story.txt`. + +## Speech Editing + +To test speech editing capabilities, use the following command: + +```bash +python src/f5_tts/infer/speech_edit.py +``` + +## Socket Realtime Client + +To communicate with socket server you need to run +```bash +python src/f5_tts/socket_server.py +``` + +
+Then create client to communicate + +``` python +import socket +import numpy as np +import asyncio +import pyaudio + +async def listen_to_voice(text, server_ip='localhost', server_port=9999): + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect((server_ip, server_port)) + + async def play_audio_stream(): + buffer = b'' + p = pyaudio.PyAudio() + stream = p.open(format=pyaudio.paFloat32, + channels=1, + rate=24000, # Ensure this matches the server's sampling rate + output=True, + frames_per_buffer=2048) + + try: + while True: + chunk = await asyncio.get_event_loop().run_in_executor(None, client_socket.recv, 1024) + if not chunk: # End of stream + break + if b"END_OF_AUDIO" in chunk: + buffer += chunk.replace(b"END_OF_AUDIO", b"") + if buffer: + audio_array = np.frombuffer(buffer, dtype=np.float32).copy() # Make a writable copy + stream.write(audio_array.tobytes()) + break + buffer += chunk + if len(buffer) >= 4096: + audio_array = np.frombuffer(buffer[:4096], dtype=np.float32).copy() # Make a writable copy + stream.write(audio_array.tobytes()) + buffer = buffer[4096:] + finally: + stream.stop_stream() + stream.close() + p.terminate() + + try: + # Send only the text to the server + await asyncio.get_event_loop().run_in_executor(None, client_socket.sendall, text.encode('utf-8')) + await play_audio_stream() + print("Audio playback finished.") + + except Exception as e: + print(f"Error in listen_to_voice: {e}") + + finally: + client_socket.close() + +# Example usage: Replace this with your actual server IP and port +async def main(): + await listen_to_voice("my name is jenny..", server_ip='localhost', server_port=9998) + +# Run the main async function +asyncio.run(main()) +``` + +
+ diff --git a/src/f5_tts/infer/examples/basic/basic.toml b/src/f5_tts/infer/examples/basic/basic.toml new file mode 100644 index 0000000000000000000000000000000000000000..7f4ab605e464febaf1cd84543e80992b9b763fd6 --- /dev/null +++ b/src/f5_tts/infer/examples/basic/basic.toml @@ -0,0 +1,10 @@ +# F5-TTS | E2-TTS +model = "F5-TTS" +ref_audio = "infer/examples/basic/basic_ref_en.wav" +# If an empty "", transcribes the reference audio automatically. +ref_text = "Some call me nature, others call me mother nature." +gen_text = "I don't really care what you call me. I've been a silent spectator, watching species evolve, empires rise and fall. But always remember, I am mighty and enduring." +# File with text to generate. Ignores the text above. +gen_file = "" +remove_silence = false +output_dir = "tests" \ No newline at end of file diff --git a/src/f5_tts/infer/examples/basic/basic_ref_en.wav b/src/f5_tts/infer/examples/basic/basic_ref_en.wav new file mode 100644 index 0000000000000000000000000000000000000000..3c593c3cfe1528432e29f355bb8fd9ec1dd9847c Binary files /dev/null and b/src/f5_tts/infer/examples/basic/basic_ref_en.wav differ diff --git a/src/f5_tts/infer/examples/basic/basic_ref_zh.wav b/src/f5_tts/infer/examples/basic/basic_ref_zh.wav new file mode 100644 index 0000000000000000000000000000000000000000..8cc055ede333a6778b7cdd5f7665a5db0e87ecd7 Binary files /dev/null and b/src/f5_tts/infer/examples/basic/basic_ref_zh.wav differ diff --git a/src/f5_tts/infer/examples/multi/country.flac b/src/f5_tts/infer/examples/multi/country.flac new file mode 100644 index 0000000000000000000000000000000000000000..ad6985b710ec754b0d40e79be6cc3563b32abf15 Binary files /dev/null and b/src/f5_tts/infer/examples/multi/country.flac differ diff --git a/src/f5_tts/infer/examples/multi/main.flac b/src/f5_tts/infer/examples/multi/main.flac new file mode 100644 index 0000000000000000000000000000000000000000..671aded6b10fc6f10d27d6b0764a109f0e7fadca Binary files /dev/null and b/src/f5_tts/infer/examples/multi/main.flac differ diff --git a/src/f5_tts/infer/examples/multi/story.toml b/src/f5_tts/infer/examples/multi/story.toml new file mode 100644 index 0000000000000000000000000000000000000000..772285f81883e67a91c5b2d0123a21717452fa7b --- /dev/null +++ b/src/f5_tts/infer/examples/multi/story.toml @@ -0,0 +1,19 @@ +# F5-TTS | E2-TTS +model = "F5-TTS" +ref_audio = "infer/examples/multi/main.flac" +# If an empty "", transcribes the reference audio automatically. +ref_text = "" +gen_text = "" +# File with text to generate. Ignores the text above. +gen_file = "infer/examples/multi/story.txt" +remove_silence = true +output_dir = "tests" + +[voices.town] +ref_audio = "infer/examples/multi/town.flac" +ref_text = "" + +[voices.country] +ref_audio = "infer/examples/multi/country.flac" +ref_text = "" + diff --git a/src/f5_tts/infer/examples/multi/story.txt b/src/f5_tts/infer/examples/multi/story.txt new file mode 100644 index 0000000000000000000000000000000000000000..bda1f2ba1b967d2e63fdaac3b987fcb54574d76f --- /dev/null +++ b/src/f5_tts/infer/examples/multi/story.txt @@ -0,0 +1 @@ +A Town Mouse and a Country Mouse were acquaintances, and the Country Mouse one day invited his friend to come and see him at his home in the fields. The Town Mouse came, and they sat down to a dinner of barleycorns and roots, the latter of which had a distinctly earthy flavour. The fare was not much to the taste of the guest, and presently he broke out with [town] “My poor dear friend, you live here no better than the ants. Now, you should just see how I fare! My larder is a regular horn of plenty. You must come and stay with me, and I promise you you shall live on the fat of the land.” [main] So when he returned to town he took the Country Mouse with him, and showed him into a larder containing flour and oatmeal and figs and honey and dates. The Country Mouse had never seen anything like it, and sat down to enjoy the luxuries his friend provided: but before they had well begun, the door of the larder opened and someone came in. The two Mice scampered off and hid themselves in a narrow and exceedingly uncomfortable hole. Presently, when all was quiet, they ventured out again; but someone else came in, and off they scuttled again. This was too much for the visitor. [country] “Goodbye,” [main] said he, [country] “I’m off. You live in the lap of luxury, I can see, but you are surrounded by dangers; whereas at home I can enjoy my simple dinner of roots and corn in peace.” \ No newline at end of file diff --git a/src/f5_tts/infer/examples/multi/town.flac b/src/f5_tts/infer/examples/multi/town.flac new file mode 100644 index 0000000000000000000000000000000000000000..39c258582ccbedc48b82c988e7808c114a682292 Binary files /dev/null and b/src/f5_tts/infer/examples/multi/town.flac differ diff --git a/src/f5_tts/infer/examples/vocab.txt b/src/f5_tts/infer/examples/vocab.txt new file mode 100644 index 0000000000000000000000000000000000000000..cd934390e8f4b3ce98eb319ae618c084d01504b5 --- /dev/null +++ b/src/f5_tts/infer/examples/vocab.txt @@ -0,0 +1,2545 @@ + +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +; += +> +? +@ +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +[ +\ +] +_ +a +a1 +ai1 +ai2 +ai3 +ai4 +an1 +an3 +an4 +ang1 +ang2 +ang4 +ao1 +ao2 +ao3 +ao4 +b +ba +ba1 +ba2 +ba3 +ba4 +bai1 +bai2 +bai3 +bai4 +ban1 +ban2 +ban3 +ban4 +bang1 +bang2 +bang3 +bang4 +bao1 +bao2 +bao3 +bao4 +bei +bei1 +bei2 +bei3 +bei4 +ben1 +ben2 +ben3 +ben4 +beng +beng1 +beng2 +beng3 +beng4 +bi1 +bi2 +bi3 +bi4 +bian1 +bian2 +bian3 +bian4 +biao1 +biao2 +biao3 +bie1 +bie2 +bie3 +bie4 +bin1 +bin4 +bing1 +bing2 +bing3 +bing4 +bo +bo1 +bo2 +bo3 +bo4 +bu2 +bu3 +bu4 +c +ca1 +cai1 +cai2 +cai3 +cai4 +can1 +can2 +can3 +can4 +cang1 +cang2 +cao1 +cao2 +cao3 +ce4 +cen1 +cen2 +ceng1 +ceng2 +ceng4 +cha1 +cha2 +cha3 +cha4 +chai1 +chai2 +chan1 +chan2 +chan3 +chan4 +chang1 +chang2 +chang3 +chang4 +chao1 +chao2 +chao3 +che1 +che2 +che3 +che4 +chen1 +chen2 +chen3 +chen4 +cheng1 +cheng2 +cheng3 +cheng4 +chi1 +chi2 +chi3 +chi4 +chong1 +chong2 +chong3 +chong4 +chou1 +chou2 +chou3 +chou4 +chu1 +chu2 +chu3 +chu4 +chua1 +chuai1 +chuai2 +chuai3 +chuai4 +chuan1 +chuan2 +chuan3 +chuan4 +chuang1 +chuang2 +chuang3 +chuang4 +chui1 +chui2 +chun1 +chun2 +chun3 +chuo1 +chuo4 +ci1 +ci2 +ci3 +ci4 +cong1 +cong2 +cou4 +cu1 +cu4 +cuan1 +cuan2 +cuan4 +cui1 +cui3 +cui4 +cun1 +cun2 +cun4 +cuo1 +cuo2 +cuo4 +d +da +da1 +da2 +da3 +da4 +dai1 +dai2 +dai3 +dai4 +dan1 +dan2 +dan3 +dan4 +dang1 +dang2 +dang3 +dang4 +dao1 +dao2 +dao3 +dao4 +de +de1 +de2 +dei3 +den4 +deng1 +deng2 +deng3 +deng4 +di1 +di2 +di3 +di4 +dia3 +dian1 +dian2 +dian3 +dian4 +diao1 +diao3 +diao4 +die1 +die2 +die4 +ding1 +ding2 +ding3 +ding4 +diu1 +dong1 +dong3 +dong4 +dou1 +dou2 +dou3 +dou4 +du1 +du2 +du3 +du4 +duan1 +duan2 +duan3 +duan4 +dui1 +dui4 +dun1 +dun3 +dun4 +duo1 +duo2 +duo3 +duo4 +e +e1 +e2 +e3 +e4 +ei2 +en1 +en4 +er +er2 +er3 +er4 +f +fa1 +fa2 +fa3 +fa4 +fan1 +fan2 +fan3 +fan4 +fang1 +fang2 +fang3 +fang4 +fei1 +fei2 +fei3 +fei4 +fen1 +fen2 +fen3 +fen4 +feng1 +feng2 +feng3 +feng4 +fo2 +fou2 +fou3 +fu1 +fu2 +fu3 +fu4 +g +ga1 +ga2 +ga3 +ga4 +gai1 +gai2 +gai3 +gai4 +gan1 +gan2 +gan3 +gan4 +gang1 +gang2 +gang3 +gang4 +gao1 +gao2 +gao3 +gao4 +ge1 +ge2 +ge3 +ge4 +gei2 +gei3 +gen1 +gen2 +gen3 +gen4 +geng1 +geng3 +geng4 +gong1 +gong3 +gong4 +gou1 +gou2 +gou3 +gou4 +gu +gu1 +gu2 +gu3 +gu4 +gua1 +gua2 +gua3 +gua4 +guai1 +guai2 +guai3 +guai4 +guan1 +guan2 +guan3 +guan4 +guang1 +guang2 +guang3 +guang4 +gui1 +gui2 +gui3 +gui4 +gun3 +gun4 +guo1 +guo2 +guo3 +guo4 +h +ha1 +ha2 +ha3 +hai1 +hai2 +hai3 +hai4 +han1 +han2 +han3 +han4 +hang1 +hang2 +hang4 +hao1 +hao2 +hao3 +hao4 +he1 +he2 +he4 +hei1 +hen2 +hen3 +hen4 +heng1 +heng2 +heng4 +hong1 +hong2 +hong3 +hong4 +hou1 +hou2 +hou3 +hou4 +hu1 +hu2 +hu3 +hu4 +hua1 +hua2 +hua4 +huai2 +huai4 +huan1 +huan2 +huan3 +huan4 +huang1 +huang2 +huang3 +huang4 +hui1 +hui2 +hui3 +hui4 +hun1 +hun2 +hun4 +huo +huo1 +huo2 +huo3 +huo4 +i +j +ji1 +ji2 +ji3 +ji4 +jia +jia1 +jia2 +jia3 +jia4 +jian1 +jian2 +jian3 +jian4 +jiang1 +jiang2 +jiang3 +jiang4 +jiao1 +jiao2 +jiao3 +jiao4 +jie1 +jie2 +jie3 +jie4 +jin1 +jin2 +jin3 +jin4 +jing1 +jing2 +jing3 +jing4 +jiong3 +jiu1 +jiu2 +jiu3 +jiu4 +ju1 +ju2 +ju3 +ju4 +juan1 +juan2 +juan3 +juan4 +jue1 +jue2 +jue4 +jun1 +jun4 +k +ka1 +ka2 +ka3 +kai1 +kai2 +kai3 +kai4 +kan1 +kan2 +kan3 +kan4 +kang1 +kang2 +kang4 +kao1 +kao2 +kao3 +kao4 +ke1 +ke2 +ke3 +ke4 +ken3 +keng1 +kong1 +kong3 +kong4 +kou1 +kou2 +kou3 +kou4 +ku1 +ku2 +ku3 +ku4 +kua1 +kua3 +kua4 +kuai3 +kuai4 +kuan1 +kuan2 +kuan3 +kuang1 +kuang2 +kuang4 +kui1 +kui2 +kui3 +kui4 +kun1 +kun3 +kun4 +kuo4 +l +la +la1 +la2 +la3 +la4 +lai2 +lai4 +lan2 +lan3 +lan4 +lang1 +lang2 +lang3 +lang4 +lao1 +lao2 +lao3 +lao4 +le +le1 +le4 +lei +lei1 +lei2 +lei3 +lei4 +leng1 +leng2 +leng3 +leng4 +li +li1 +li2 +li3 +li4 +lia3 +lian2 +lian3 +lian4 +liang2 +liang3 +liang4 +liao1 +liao2 +liao3 +liao4 +lie1 +lie2 +lie3 +lie4 +lin1 +lin2 +lin3 +lin4 +ling2 +ling3 +ling4 +liu1 +liu2 +liu3 +liu4 +long1 +long2 +long3 +long4 +lou1 +lou2 +lou3 +lou4 +lu1 +lu2 +lu3 +lu4 +luan2 +luan3 +luan4 +lun1 +lun2 +lun4 +luo1 +luo2 +luo3 +luo4 +lv2 +lv3 +lv4 +lve3 +lve4 +m +ma +ma1 +ma2 +ma3 +ma4 +mai2 +mai3 +mai4 +man1 +man2 +man3 +man4 +mang2 +mang3 +mao1 +mao2 +mao3 +mao4 +me +mei2 +mei3 +mei4 +men +men1 +men2 +men4 +meng +meng1 +meng2 +meng3 +meng4 +mi1 +mi2 +mi3 +mi4 +mian2 +mian3 +mian4 +miao1 +miao2 +miao3 +miao4 +mie1 +mie4 +min2 +min3 +ming2 +ming3 +ming4 +miu4 +mo1 +mo2 +mo3 +mo4 +mou1 +mou2 +mou3 +mu2 +mu3 +mu4 +n +n2 +na1 +na2 +na3 +na4 +nai2 +nai3 +nai4 +nan1 +nan2 +nan3 +nan4 +nang1 +nang2 +nang3 +nao1 +nao2 +nao3 +nao4 +ne +ne2 +ne4 +nei3 +nei4 +nen4 +neng2 +ni1 +ni2 +ni3 +ni4 +nian1 +nian2 +nian3 +nian4 +niang2 +niang4 +niao2 +niao3 +niao4 +nie1 +nie4 +nin2 +ning2 +ning3 +ning4 +niu1 +niu2 +niu3 +niu4 +nong2 +nong4 +nou4 +nu2 +nu3 +nu4 +nuan3 +nuo2 +nuo4 +nv2 +nv3 +nve4 +o +o1 +o2 +ou1 +ou2 +ou3 +ou4 +p +pa1 +pa2 +pa4 +pai1 +pai2 +pai3 +pai4 +pan1 +pan2 +pan4 +pang1 +pang2 +pang4 +pao1 +pao2 +pao3 +pao4 +pei1 +pei2 +pei4 +pen1 +pen2 +pen4 +peng1 +peng2 +peng3 +peng4 +pi1 +pi2 +pi3 +pi4 +pian1 +pian2 +pian4 +piao1 +piao2 +piao3 +piao4 +pie1 +pie2 +pie3 +pin1 +pin2 +pin3 +pin4 +ping1 +ping2 +po1 +po2 +po3 +po4 +pou1 +pu1 +pu2 +pu3 +pu4 +q +qi1 +qi2 +qi3 +qi4 +qia1 +qia3 +qia4 +qian1 +qian2 +qian3 +qian4 +qiang1 +qiang2 +qiang3 +qiang4 +qiao1 +qiao2 +qiao3 +qiao4 +qie1 +qie2 +qie3 +qie4 +qin1 +qin2 +qin3 +qin4 +qing1 +qing2 +qing3 +qing4 +qiong1 +qiong2 +qiu1 +qiu2 +qiu3 +qu1 +qu2 +qu3 +qu4 +quan1 +quan2 +quan3 +quan4 +que1 +que2 +que4 +qun2 +r +ran2 +ran3 +rang1 +rang2 +rang3 +rang4 +rao2 +rao3 +rao4 +re2 +re3 +re4 +ren2 +ren3 +ren4 +reng1 +reng2 +ri4 +rong1 +rong2 +rong3 +rou2 +rou4 +ru2 +ru3 +ru4 +ruan2 +ruan3 +rui3 +rui4 +run4 +ruo4 +s +sa1 +sa2 +sa3 +sa4 +sai1 +sai4 +san1 +san2 +san3 +san4 +sang1 +sang3 +sang4 +sao1 +sao2 +sao3 +sao4 +se4 +sen1 +seng1 +sha1 +sha2 +sha3 +sha4 +shai1 +shai2 +shai3 +shai4 +shan1 +shan3 +shan4 +shang +shang1 +shang3 +shang4 +shao1 +shao2 +shao3 +shao4 +she1 +she2 +she3 +she4 +shei2 +shen1 +shen2 +shen3 +shen4 +sheng1 +sheng2 +sheng3 +sheng4 +shi +shi1 +shi2 +shi3 +shi4 +shou1 +shou2 +shou3 +shou4 +shu1 +shu2 +shu3 +shu4 +shua1 +shua2 +shua3 +shua4 +shuai1 +shuai3 +shuai4 +shuan1 +shuan4 +shuang1 +shuang3 +shui2 +shui3 +shui4 +shun3 +shun4 +shuo1 +shuo4 +si1 +si2 +si3 +si4 +song1 +song3 +song4 +sou1 +sou3 +sou4 +su1 +su2 +su4 +suan1 +suan4 +sui1 +sui2 +sui3 +sui4 +sun1 +sun3 +suo +suo1 +suo2 +suo3 +t +ta1 +ta2 +ta3 +ta4 +tai1 +tai2 +tai4 +tan1 +tan2 +tan3 +tan4 +tang1 +tang2 +tang3 +tang4 +tao1 +tao2 +tao3 +tao4 +te4 +teng2 +ti1 +ti2 +ti3 +ti4 +tian1 +tian2 +tian3 +tiao1 +tiao2 +tiao3 +tiao4 +tie1 +tie2 +tie3 +tie4 +ting1 +ting2 +ting3 +tong1 +tong2 +tong3 +tong4 +tou +tou1 +tou2 +tou4 +tu1 +tu2 +tu3 +tu4 +tuan1 +tuan2 +tui1 +tui2 +tui3 +tui4 +tun1 +tun2 +tun4 +tuo1 +tuo2 +tuo3 +tuo4 +u +v +w +wa +wa1 +wa2 +wa3 +wa4 +wai1 +wai3 +wai4 +wan1 +wan2 +wan3 +wan4 +wang1 +wang2 +wang3 +wang4 +wei1 +wei2 +wei3 +wei4 +wen1 +wen2 +wen3 +wen4 +weng1 +weng4 +wo1 +wo2 +wo3 +wo4 +wu1 +wu2 +wu3 +wu4 +x +xi1 +xi2 +xi3 +xi4 +xia1 +xia2 +xia4 +xian1 +xian2 +xian3 +xian4 +xiang1 +xiang2 +xiang3 +xiang4 +xiao1 +xiao2 +xiao3 +xiao4 +xie1 +xie2 +xie3 +xie4 +xin1 +xin2 +xin4 +xing1 +xing2 +xing3 +xing4 +xiong1 +xiong2 +xiu1 +xiu3 +xiu4 +xu +xu1 +xu2 +xu3 +xu4 +xuan1 +xuan2 +xuan3 +xuan4 +xue1 +xue2 +xue3 +xue4 +xun1 +xun2 +xun4 +y +ya +ya1 +ya2 +ya3 +ya4 +yan1 +yan2 +yan3 +yan4 +yang1 +yang2 +yang3 +yang4 +yao1 +yao2 +yao3 +yao4 +ye1 +ye2 +ye3 +ye4 +yi +yi1 +yi2 +yi3 +yi4 +yin1 +yin2 +yin3 +yin4 +ying1 +ying2 +ying3 +ying4 +yo1 +yong1 +yong2 +yong3 +yong4 +you1 +you2 +you3 +you4 +yu1 +yu2 +yu3 +yu4 +yuan1 +yuan2 +yuan3 +yuan4 +yue1 +yue4 +yun1 +yun2 +yun3 +yun4 +z +za1 +za2 +za3 +zai1 +zai3 +zai4 +zan1 +zan2 +zan3 +zan4 +zang1 +zang4 +zao1 +zao2 +zao3 +zao4 +ze2 +ze4 +zei2 +zen3 +zeng1 +zeng4 +zha1 +zha2 +zha3 +zha4 +zhai1 +zhai2 +zhai3 +zhai4 +zhan1 +zhan2 +zhan3 +zhan4 +zhang1 +zhang2 +zhang3 +zhang4 +zhao1 +zhao2 +zhao3 +zhao4 +zhe +zhe1 +zhe2 +zhe3 +zhe4 +zhen1 +zhen2 +zhen3 +zhen4 +zheng1 +zheng2 +zheng3 +zheng4 +zhi1 +zhi2 +zhi3 +zhi4 +zhong1 +zhong2 +zhong3 +zhong4 +zhou1 +zhou2 +zhou3 +zhou4 +zhu1 +zhu2 +zhu3 +zhu4 +zhua1 +zhua2 +zhua3 +zhuai1 +zhuai3 +zhuai4 +zhuan1 +zhuan2 +zhuan3 +zhuan4 +zhuang1 +zhuang4 +zhui1 +zhui4 +zhun1 +zhun2 +zhun3 +zhuo1 +zhuo2 +zi +zi1 +zi2 +zi3 +zi4 +zong1 +zong2 +zong3 +zong4 +zou1 +zou2 +zou3 +zou4 +zu1 +zu2 +zu3 +zuan1 +zuan3 +zuan4 +zui2 +zui3 +zui4 +zun1 +zuo +zuo1 +zuo2 +zuo3 +zuo4 +{ +~ +¡ +¢ +£ +¥ +§ +¨ +© +« +® +¯ +° +± +² +³ +´ +µ +· +¹ +º +» +¼ +½ +¾ +¿ +À +Á + +à +Ä +Å +Æ +Ç +È +É +Ê +Í +Î +Ñ +Ó +Ö +× +Ø +Ú +Ü +Ý +Þ +ß +à +á +â +ã +ä +å +æ +ç +è +é +ê +ë +ì +í +î +ï +ð +ñ +ò +ó +ô +õ +ö +ø +ù +ú +û +ü +ý +Ā +ā +ă +ą +ć +Č +č +Đ +đ +ē +ė +ę +ě +ĝ +ğ +ħ +ī +į +İ +ı +Ł +ł +ń +ņ +ň +ŋ +Ō +ō +ő +œ +ř +Ś +ś +Ş +ş +Š +š +Ť +ť +ũ +ū +ź +Ż +ż +Ž +ž +ơ +ư +ǎ +ǐ +ǒ +ǔ +ǚ +ș +ț +ɑ +ɔ +ɕ +ə +ɛ +ɜ +ɡ +ɣ +ɪ +ɫ +ɴ +ɹ +ɾ +ʃ +ʊ +ʌ +ʒ +ʔ +ʰ +ʷ +ʻ +ʾ +ʿ +ˈ +ː +˙ +˜ +ˢ +́ +̅ +Α +Β +Δ +Ε +Θ +Κ +Λ +Μ +Ξ +Π +Σ +Τ +Φ +Χ +Ψ +Ω +ά +έ +ή +ί +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +ϊ +ό +ύ +ώ +ϕ +ϵ +Ё +А +Б +В +Г +Д +Е +Ж +З +И +Й +К +Л +М +Н +О +П +Р +С +Т +У +Ф +Х +Ц +Ч +Ш +Щ +Ы +Ь +Э +Ю +Я +а +б +в +г +д +е +ж +з +и +й +к +л +м +н +о +п +р +с +т +у +ф +х +ц +ч +ш +щ +ъ +ы +ь +э +ю +я +ё +і +ְ +ִ +ֵ +ֶ +ַ +ָ +ֹ +ּ +־ +ׁ +א +ב +ג +ד +ה +ו +ז +ח +ט +י +כ +ל +ם +מ +ן +נ +ס +ע +פ +ק +ר +ש +ת +أ +ب +ة +ت +ج +ح +د +ر +ز +س +ص +ط +ع +ق +ك +ل +م +ن +ه +و +ي +َ +ُ +ِ +ْ +ก +ข +ง +จ +ต +ท +น +ป +ย +ร +ว +ส +ห +อ +ฮ +ั +า +ี +ึ +โ +ใ +ไ +่ +้ +์ +ḍ +Ḥ +ḥ +ṁ +ṃ +ṅ +ṇ +Ṛ +ṛ +Ṣ +ṣ +Ṭ +ṭ +ạ +ả +Ấ +ấ +ầ +ậ +ắ +ằ +ẻ +ẽ +ế +ề +ể +ễ +ệ +ị +ọ +ỏ +ố +ồ +ộ +ớ +ờ +ở +ụ +ủ +ứ +ữ +ἀ +ἁ +Ἀ +ἐ +ἔ +ἰ +ἱ +ὀ +ὁ +ὐ +ὲ +ὸ +ᾶ +᾽ +ῆ +ῇ +ῶ +‎ +‑ +‒ +– +— +― +‖ +† +‡ +• +… +‧ +‬ +′ +″ +⁄ +⁡ +⁰ +⁴ +⁵ +⁶ +⁷ +⁸ +⁹ +₁ +₂ +₃ +€ +₱ +₹ +₽ +℃ +ℏ +ℓ +№ +ℝ +™ +⅓ +⅔ +⅛ +→ +∂ +∈ +∑ +− +∗ +√ +∞ +∫ +≈ +≠ +≡ +≤ +≥ +⋅ +⋯ +█ +♪ +⟨ +⟩ +、 +。 +《 +》 +「 +」 +【 +】 +あ +う +え +お +か +が +き +ぎ +く +ぐ +け +げ +こ +ご +さ +し +じ +す +ず +せ +ぜ +そ +ぞ +た +だ +ち +っ +つ +で +と +ど +な +に +ね +の +は +ば +ひ +ぶ +へ +べ +ま +み +む +め +も +ゃ +や +ゆ +ょ +よ +ら +り +る +れ +ろ +わ +を +ん +ァ +ア +ィ +イ +ウ +ェ +エ +オ +カ +ガ +キ +ク +ケ +ゲ +コ +ゴ +サ +ザ +シ +ジ +ス +ズ +セ +ゾ +タ +ダ +チ +ッ +ツ +テ +デ +ト +ド +ナ +ニ +ネ +ノ +バ +パ +ビ +ピ +フ +プ +ヘ +ベ +ペ +ホ +ボ +ポ +マ +ミ +ム +メ +モ +ャ +ヤ +ュ +ユ +ョ +ヨ +ラ +リ +ル +レ +ロ +ワ +ン +・ +ー +ㄋ +ㄍ +ㄎ +ㄏ +ㄓ +ㄕ +ㄚ +ㄜ +ㄟ +ㄤ +ㄥ +ㄧ +ㄱ +ㄴ +ㄷ +ㄹ +ㅁ +ㅂ +ㅅ +ㅈ +ㅍ +ㅎ +ㅏ +ㅓ +ㅗ +ㅜ +ㅡ +ㅣ +㗎 +가 +각 +간 +갈 +감 +갑 +갓 +갔 +강 +같 +개 +거 +건 +걸 +겁 +것 +겉 +게 +겠 +겨 +결 +겼 +경 +계 +고 +곤 +골 +곱 +공 +과 +관 +광 +교 +구 +국 +굴 +귀 +귄 +그 +근 +글 +금 +기 +긴 +길 +까 +깍 +깔 +깜 +깨 +께 +꼬 +꼭 +꽃 +꾸 +꿔 +끔 +끗 +끝 +끼 +나 +난 +날 +남 +납 +내 +냐 +냥 +너 +넘 +넣 +네 +녁 +년 +녕 +노 +녹 +놀 +누 +눈 +느 +는 +늘 +니 +님 +닙 +다 +닥 +단 +달 +닭 +당 +대 +더 +덕 +던 +덥 +데 +도 +독 +동 +돼 +됐 +되 +된 +될 +두 +둑 +둥 +드 +들 +등 +디 +따 +딱 +딸 +땅 +때 +떤 +떨 +떻 +또 +똑 +뚱 +뛰 +뜻 +띠 +라 +락 +란 +람 +랍 +랑 +래 +랜 +러 +런 +럼 +렇 +레 +려 +력 +렵 +렸 +로 +록 +롬 +루 +르 +른 +를 +름 +릉 +리 +릴 +림 +마 +막 +만 +많 +말 +맑 +맙 +맛 +매 +머 +먹 +멍 +메 +면 +명 +몇 +모 +목 +몸 +못 +무 +문 +물 +뭐 +뭘 +미 +민 +밌 +밑 +바 +박 +밖 +반 +받 +발 +밤 +밥 +방 +배 +백 +밸 +뱀 +버 +번 +벌 +벚 +베 +벼 +벽 +별 +병 +보 +복 +본 +볼 +봐 +봤 +부 +분 +불 +비 +빔 +빛 +빠 +빨 +뼈 +뽀 +뿅 +쁘 +사 +산 +살 +삼 +샀 +상 +새 +색 +생 +서 +선 +설 +섭 +섰 +성 +세 +셔 +션 +셨 +소 +속 +손 +송 +수 +숙 +순 +술 +숫 +숭 +숲 +쉬 +쉽 +스 +슨 +습 +슷 +시 +식 +신 +실 +싫 +심 +십 +싶 +싸 +써 +쓰 +쓴 +씌 +씨 +씩 +씬 +아 +악 +안 +않 +알 +야 +약 +얀 +양 +얘 +어 +언 +얼 +엄 +업 +없 +었 +엉 +에 +여 +역 +연 +염 +엽 +영 +옆 +예 +옛 +오 +온 +올 +옷 +옹 +와 +왔 +왜 +요 +욕 +용 +우 +운 +울 +웃 +워 +원 +월 +웠 +위 +윙 +유 +육 +윤 +으 +은 +을 +음 +응 +의 +이 +익 +인 +일 +읽 +임 +입 +있 +자 +작 +잔 +잖 +잘 +잡 +잤 +장 +재 +저 +전 +점 +정 +제 +져 +졌 +조 +족 +좀 +종 +좋 +죠 +주 +준 +줄 +중 +줘 +즈 +즐 +즘 +지 +진 +집 +짜 +짝 +쩌 +쪼 +쪽 +쫌 +쭈 +쯔 +찌 +찍 +차 +착 +찾 +책 +처 +천 +철 +체 +쳐 +쳤 +초 +촌 +추 +출 +춤 +춥 +춰 +치 +친 +칠 +침 +칩 +칼 +커 +켓 +코 +콩 +쿠 +퀴 +크 +큰 +큽 +키 +킨 +타 +태 +터 +턴 +털 +테 +토 +통 +투 +트 +특 +튼 +틀 +티 +팀 +파 +팔 +패 +페 +펜 +펭 +평 +포 +폭 +표 +품 +풍 +프 +플 +피 +필 +하 +학 +한 +할 +함 +합 +항 +해 +햇 +했 +행 +허 +험 +형 +혜 +호 +혼 +홀 +화 +회 +획 +후 +휴 +흐 +흔 +희 +히 +힘 +ﷺ +ﷻ +! +, +? +� +𠮶 diff --git a/src/f5_tts/infer/infer_cli.py b/src/f5_tts/infer/infer_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..2803558ef500702e86c6f36ee69bde5e98fad1fa --- /dev/null +++ b/src/f5_tts/infer/infer_cli.py @@ -0,0 +1,220 @@ +import argparse +import codecs +import os +import re +from importlib.resources import files +from pathlib import Path + +import numpy as np +import soundfile as sf +import tomli +from cached_path import cached_path + +from f5_tts.infer.utils_infer import ( + infer_process, + load_model, + load_vocoder, + preprocess_ref_audio_text, + remove_silence_for_generated_wav, +) +from f5_tts.model import DiT, UNetT + +parser = argparse.ArgumentParser( + prog="python3 infer-cli.py", + description="Commandline interface for E2/F5 TTS with Advanced Batch Processing.", + epilog="Specify options above to override one or more settings from config.", +) +parser.add_argument( + "-c", + "--config", + help="Configuration file. Default=infer/examples/basic/basic.toml", + default=os.path.join(files("f5_tts").joinpath("infer/examples/basic"), "basic.toml"), +) +parser.add_argument( + "-m", + "--model", + help="F5-TTS | E2-TTS", +) +parser.add_argument( + "-p", + "--ckpt_file", + help="The Checkpoint .pt", +) +parser.add_argument( + "-v", + "--vocab_file", + help="The vocab .txt", +) +parser.add_argument("-r", "--ref_audio", type=str, help="Reference audio file < 15 seconds.") +parser.add_argument("-s", "--ref_text", type=str, default="666", help="Subtitle for the reference audio.") +parser.add_argument( + "-t", + "--gen_text", + type=str, + help="Text to generate.", +) +parser.add_argument( + "-f", + "--gen_file", + type=str, + help="File with text to generate. Ignores --text", +) +parser.add_argument( + "-o", + "--output_dir", + type=str, + help="Path to output folder..", +) +parser.add_argument( + "--remove_silence", + help="Remove silence.", +) +parser.add_argument("--vocoder_name", type=str, default="vocos", choices=["vocos", "bigvgan"], help="vocoder name") +parser.add_argument( + "--load_vocoder_from_local", + action="store_true", + help="load vocoder from local. Default: ../checkpoints/charactr/vocos-mel-24khz", +) +parser.add_argument( + "--speed", + type=float, + default=1.0, + help="Adjust the speed of the audio generation (default: 1.0)", +) +args = parser.parse_args() + +config = tomli.load(open(args.config, "rb")) + +ref_audio = args.ref_audio if args.ref_audio else config["ref_audio"] +ref_text = args.ref_text if args.ref_text != "666" else config["ref_text"] +gen_text = args.gen_text if args.gen_text else config["gen_text"] +gen_file = args.gen_file if args.gen_file else config["gen_file"] + +# patches for pip pkg user +if "infer/examples/" in ref_audio: + ref_audio = str(files("f5_tts").joinpath(f"{ref_audio}")) +if "infer/examples/" in gen_file: + gen_file = str(files("f5_tts").joinpath(f"{gen_file}")) +if "voices" in config: + for voice in config["voices"]: + voice_ref_audio = config["voices"][voice]["ref_audio"] + if "infer/examples/" in voice_ref_audio: + config["voices"][voice]["ref_audio"] = str(files("f5_tts").joinpath(f"{voice_ref_audio}")) + +if gen_file: + gen_text = codecs.open(gen_file, "r", "utf-8").read() +output_dir = args.output_dir if args.output_dir else config["output_dir"] +model = args.model if args.model else config["model"] +ckpt_file = args.ckpt_file if args.ckpt_file else "" +vocab_file = args.vocab_file if args.vocab_file else "" +remove_silence = args.remove_silence if args.remove_silence else config["remove_silence"] +speed = args.speed +wave_path = Path(output_dir) / "infer_cli_out.wav" +# spectrogram_path = Path(output_dir) / "infer_cli_out.png" +if args.vocoder_name == "vocos": + vocoder_local_path = "../checkpoints/vocos-mel-24khz" +elif args.vocoder_name == "bigvgan": + vocoder_local_path = "../checkpoints/bigvgan_v2_24khz_100band_256x" +mel_spec_type = args.vocoder_name + +vocoder = load_vocoder(vocoder_name=mel_spec_type, is_local=args.load_vocoder_from_local, local_path=vocoder_local_path) + + +# load models +if model == "F5-TTS": + model_cls = DiT + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + if ckpt_file == "": + if args.vocoder_name == "vocos": + repo_name = "F5-TTS" + exp_name = "F5TTS_Base" + ckpt_step = 1200000 + ckpt_file = str(cached_path(f"hf://SWivid/{repo_name}/{exp_name}/model_{ckpt_step}.safetensors")) + # ckpt_file = f"ckpts/{exp_name}/model_{ckpt_step}.pt" # .pt | .safetensors; local path + elif args.vocoder_name == "bigvgan": + repo_name = "F5-TTS" + exp_name = "F5TTS_Base_bigvgan" + ckpt_step = 1250000 + ckpt_file = str(cached_path(f"hf://SWivid/{repo_name}/{exp_name}/model_{ckpt_step}.pt")) + +elif model == "E2-TTS": + model_cls = UNetT + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + if ckpt_file == "": + repo_name = "E2-TTS" + exp_name = "E2TTS_Base" + ckpt_step = 1200000 + ckpt_file = str(cached_path(f"hf://SWivid/{repo_name}/{exp_name}/model_{ckpt_step}.safetensors")) + # ckpt_file = f"ckpts/{exp_name}/model_{ckpt_step}.pt" # .pt | .safetensors; local path + elif args.vocoder_name == "bigvgan": # TODO: need to test + repo_name = "F5-TTS" + exp_name = "F5TTS_Base_bigvgan" + ckpt_step = 1250000 + ckpt_file = str(cached_path(f"hf://SWivid/{repo_name}/{exp_name}/model_{ckpt_step}.pt")) + + +print(f"Using {model}...") +ema_model = load_model(model_cls, model_cfg, ckpt_file, mel_spec_type=args.vocoder_name, vocab_file=vocab_file) + + +def main_process(ref_audio, ref_text, text_gen, model_obj, mel_spec_type, remove_silence, speed): + main_voice = {"ref_audio": ref_audio, "ref_text": ref_text} + if "voices" not in config: + voices = {"main": main_voice} + else: + voices = config["voices"] + voices["main"] = main_voice + for voice in voices: + voices[voice]["ref_audio"], voices[voice]["ref_text"] = preprocess_ref_audio_text( + voices[voice]["ref_audio"], voices[voice]["ref_text"] + ) + print("Voice:", voice) + print("Ref_audio:", voices[voice]["ref_audio"]) + print("Ref_text:", voices[voice]["ref_text"]) + + generated_audio_segments = [] + reg1 = r"(?=\[\w+\])" + chunks = re.split(reg1, text_gen) + reg2 = r"\[(\w+)\]" + for text in chunks: + if not text.strip(): + continue + match = re.match(reg2, text) + if match: + voice = match[1] + else: + print("No voice tag found, using main.") + voice = "main" + if voice not in voices: + print(f"Voice {voice} not found, using main.") + voice = "main" + text = re.sub(reg2, "", text) + gen_text = text.strip() + ref_audio = voices[voice]["ref_audio"] + ref_text = voices[voice]["ref_text"] + print(f"Voice: {voice}") + audio, final_sample_rate, spectragram = infer_process( + ref_audio, ref_text, gen_text, model_obj, vocoder, mel_spec_type=mel_spec_type, speed=speed + ) + generated_audio_segments.append(audio) + + if generated_audio_segments: + final_wave = np.concatenate(generated_audio_segments) + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open(wave_path, "wb") as f: + sf.write(f.name, final_wave, final_sample_rate) + # Remove silence + if remove_silence: + remove_silence_for_generated_wav(f.name) + print(f.name) + + +def main(): + main_process(ref_audio, ref_text, gen_text, ema_model, mel_spec_type, remove_silence, speed) + + +if __name__ == "__main__": + main() diff --git a/src/f5_tts/infer/infer_gradio.py b/src/f5_tts/infer/infer_gradio.py new file mode 100644 index 0000000000000000000000000000000000000000..c05ee1ba699fc215e4c0cba24ff9900507d6b6fb --- /dev/null +++ b/src/f5_tts/infer/infer_gradio.py @@ -0,0 +1,744 @@ +# ruff: noqa: E402 +# Above allows ruff to ignore E402: module level import not at top of file + +import re +import tempfile + +import click +import gradio as gr +import numpy as np +import soundfile as sf +import torchaudio +from cached_path import cached_path +from transformers import AutoModelForCausalLM, AutoTokenizer +from num2words import num2words + +try: + import spaces + + USING_SPACES = True +except ImportError: + USING_SPACES = False + + +def gpu_decorator(func): + if USING_SPACES: + return spaces.GPU(func) + else: + return func + + +from f5_tts.model import DiT, UNetT +from f5_tts.infer.utils_infer import ( + load_vocoder, + load_model, + preprocess_ref_audio_text, + infer_process, + remove_silence_for_generated_wav, + save_spectrogram, +) + +vocoder = load_vocoder() + + +# load models +F5TTS_model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) +F5TTS_ema_model = load_model( + DiT, F5TTS_model_cfg, str(cached_path("hf://jpgallegoar/F5-Spanish/model_1200000.safetensors")) +) + +chat_model_state = None +chat_tokenizer_state = None + + +@gpu_decorator +def generate_response(messages, model, tokenizer): + """Generate response using Qwen""" + text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + ) + + model_inputs = tokenizer([text], return_tensors="pt").to(model.device) + generated_ids = model.generate( + **model_inputs, + max_new_tokens=512, + temperature=0.7, + top_p=0.95, + ) + + generated_ids = [ + output_ids[len(input_ids) :] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + ] + return tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] + +def traducir_numero_a_texto(texto): + texto_separado = re.sub(r'([A-Za-z])(\d)', r'\1 \2', texto) + texto_separado = re.sub(r'(\d)([A-Za-z])', r'\1 \2', texto_separado) + + def reemplazar_numero(match): + numero = match.group() + return num2words(int(numero), lang='es') + + texto_traducido = re.sub(r'\b\d+\b', reemplazar_numero, texto_separado) + + return texto_traducido + +@gpu_decorator +def infer( + ref_audio_orig, ref_text, gen_text, model, remove_silence, cross_fade_duration=0.15, speed=1, show_info=gr.Info +): + ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_orig, ref_text, show_info=show_info) + + ema_model = F5TTS_ema_model + + if not gen_text.startswith(" "): + gen_text = " " + gen_text + if not gen_text.endswith(". "): + gen_text += ". " + + gen_text = gen_text.lower() + gen_text = traducir_numero_a_texto(gen_text) + + final_wave, final_sample_rate, combined_spectrogram = infer_process( + ref_audio, + ref_text, + gen_text, + ema_model, + vocoder, + cross_fade_duration=cross_fade_duration, + speed=speed, + show_info=show_info, + progress=gr.Progress(), + ) + + # Remove silence + if remove_silence: + with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f: + sf.write(f.name, final_wave, final_sample_rate) + remove_silence_for_generated_wav(f.name) + final_wave, _ = torchaudio.load(f.name) + final_wave = final_wave.squeeze().cpu().numpy() + + # Save the spectrogram + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_spectrogram: + spectrogram_path = tmp_spectrogram.name + save_spectrogram(combined_spectrogram, spectrogram_path) + + return (final_sample_rate, final_wave), spectrogram_path + + +with gr.Blocks() as app_credits: + gr.Markdown(""" +# Créditos + +* [mrfakename](https://github.com/fakerybakery) por el [demo online original](https://huggingface.co/spaces/mrfakename/E2-F5-TTS) +* [RootingInLoad](https://github.com/RootingInLoad) por la generación inicial de fragmentos y exploración de la aplicación de podcast +* [jpgallegoar](https://github.com/jpgallegoar) por la generación de múltiples tipos de habla, chat de voz y afinación en español +""") + + +with gr.Blocks() as app_tts: + gr.Markdown("# TTS por Lotes") + ref_audio_input = gr.Audio(label="Audio de Referencia", type="filepath") + gen_text_input = gr.Textbox(label="Texto para Generar", lines=10) + model_choice = gr.Radio(choices=["F5-TTS"], label="Seleccionar Modelo TTS", value="F5-TTS") + generate_btn = gr.Button("Sintetizar", variant="primary") + with gr.Accordion("Configuraciones Avanzadas", open=False): + ref_text_input = gr.Textbox( + label="Texto de Referencia", + info="Deja en blanco para transcribir automáticamente el audio de referencia. Si ingresas texto, sobrescribirá la transcripción automática.", + lines=2, + ) + remove_silence = gr.Checkbox( + label="Eliminar Silencios", + info="El modelo tiende a producir silencios, especialmente en audios más largos. Podemos eliminar manualmente los silencios si es necesario. Ten en cuenta que esta es una característica experimental y puede producir resultados extraños. Esto también aumentará el tiempo de generación.", + value=False, + ) + speed_slider = gr.Slider( + label="Velocidad", + minimum=0.3, + maximum=2.0, + value=1.0, + step=0.1, + info="Ajusta la velocidad del audio.", + ) + cross_fade_duration_slider = gr.Slider( + label="Duración del Cross-Fade (s)", + minimum=0.0, + maximum=1.0, + value=0.15, + step=0.01, + info="Establece la duración del cross-fade entre clips de audio.", + ) + + audio_output = gr.Audio(label="Audio Sintetizado") + spectrogram_output = gr.Image(label="Espectrograma") + + generate_btn.click( + infer, + inputs=[ + ref_audio_input, + ref_text_input, + gen_text_input, + model_choice, + remove_silence, + cross_fade_duration_slider, + speed_slider, + ], + outputs=[audio_output, spectrogram_output], + ) + + +def parse_speechtypes_text(gen_text): + # Pattern to find {speechtype} + pattern = r"\{(.*?)\}" + + # Split the text by the pattern + tokens = re.split(pattern, gen_text) + + segments = [] + + current_style = "Regular" + + for i in range(len(tokens)): + if i % 2 == 0: + # This is text + text = tokens[i].strip() + if text: + segments.append({"style": current_style, "text": text}) + else: + # This is style + style = tokens[i].strip() + current_style = style + + return segments + + +with gr.Blocks() as app_multistyle: + # New section for multistyle generation + gr.Markdown( + """ + # Generación de Múltiples Tipos de Habla + + Esta sección te permite generar múltiples tipos de habla o las voces de múltiples personas. Ingresa tu texto en el formato mostrado a continuación, y el sistema generará el habla utilizando el tipo apropiado. Si no se especifica, el modelo utilizará el tipo de habla regular. El tipo de habla actual se usará hasta que se especifique el siguiente tipo de habla. + """ + ) + + with gr.Row(): + gr.Markdown( + """ + **Entrada de Ejemplo:** + {Regular} Hola, me gustaría pedir un sándwich, por favor. + {Sorprendido} ¿Qué quieres decir con que no tienen pan? + {Triste} Realmente quería un sándwich... + {Enojado} ¡Sabes qué, maldición a ti y a tu pequeña tienda! + {Susurro} Solo volveré a casa y lloraré ahora. + {Gritando} ¿Por qué yo?! + """ + ) + + gr.Markdown( + """ + **Entrada de Ejemplo 2:** + {Speaker1_Feliz} Hola, me gustaría pedir un sándwich, por favor. + {Speaker2_Regular} Lo siento, nos hemos quedado sin pan. + {Speaker1_Triste} Realmente quería un sándwich... + {Speaker2_Susurro} Te daré el último que estaba escondiendo. + """ + ) + + gr.Markdown( + "Sube diferentes clips de audio para cada tipo de habla. El primer tipo de habla es obligatorio. Puedes agregar tipos de habla adicionales haciendo clic en el botón 'Agregar Tipo de Habla'." + ) + + # Regular speech type (mandatory) + with gr.Row(): + with gr.Column(): + regular_name = gr.Textbox(value="Regular", label="Nombre del Tipo de Habla") + regular_insert = gr.Button("Insertar", variant="secondary") + regular_audio = gr.Audio(label="Audio de Referencia Regular", type="filepath") + regular_ref_text = gr.Textbox(label="Texto de Referencia (Regular)", lines=2) + + # Additional speech types (up to 99 more) + max_speech_types = 100 + speech_type_rows = [] + speech_type_names = [regular_name] + speech_type_audios = [] + speech_type_ref_texts = [] + speech_type_delete_btns = [] + speech_type_insert_btns = [] + speech_type_insert_btns.append(regular_insert) + + for i in range(max_speech_types - 1): + with gr.Row(visible=False) as row: + with gr.Column(): + name_input = gr.Textbox(label="Nombre del Tipo de Habla") + delete_btn = gr.Button("Eliminar", variant="secondary") + insert_btn = gr.Button("Insertar", variant="secondary") + audio_input = gr.Audio(label="Audio de Referencia", type="filepath") + ref_text_input = gr.Textbox(label="Texto de Referencia", lines=2) + speech_type_rows.append(row) + speech_type_names.append(name_input) + speech_type_audios.append(audio_input) + speech_type_ref_texts.append(ref_text_input) + speech_type_delete_btns.append(delete_btn) + speech_type_insert_btns.append(insert_btn) + + # Button to add speech type + add_speech_type_btn = gr.Button("Agregar Tipo de Habla") + + # Keep track of current number of speech types + speech_type_count = gr.State(value=0) + + # Function to add a speech type + def add_speech_type_fn(speech_type_count): + if speech_type_count < max_speech_types - 1: + speech_type_count += 1 + # Prepare updates for the rows + row_updates = [] + for i in range(max_speech_types - 1): + if i < speech_type_count: + row_updates.append(gr.update(visible=True)) + else: + row_updates.append(gr.update()) + else: + # Optionally, show a warning + row_updates = [gr.update() for _ in range(max_speech_types - 1)] + return [speech_type_count] + row_updates + + add_speech_type_btn.click( + add_speech_type_fn, inputs=speech_type_count, outputs=[speech_type_count] + speech_type_rows + ) + + # Function to delete a speech type + def make_delete_speech_type_fn(index): + def delete_speech_type_fn(speech_type_count): + # Prepare updates + row_updates = [] + + for i in range(max_speech_types - 1): + if i == index: + row_updates.append(gr.update(visible=False)) + else: + row_updates.append(gr.update()) + + speech_type_count = max(0, speech_type_count - 1) + + return [speech_type_count] + row_updates + + return delete_speech_type_fn + + # Update delete button clicks + for i, delete_btn in enumerate(speech_type_delete_btns): + delete_fn = make_delete_speech_type_fn(i) + delete_btn.click(delete_fn, inputs=speech_type_count, outputs=[speech_type_count] + speech_type_rows) + + # Text input for the prompt + gen_text_input_multistyle = gr.Textbox( + label="Texto para Generar", + lines=10, + placeholder="Ingresa el guion con los nombres de los hablantes (o tipos de emociones) al inicio de cada bloque, por ejemplo:\n\n{Regular} Hola, me gustaría pedir un sándwich, por favor.\n{Sorprendido} ¿Qué quieres decir con que no tienen pan?\n{Triste} Realmente quería un sándwich...\n{Enojado} ¡Sabes qué, maldición a ti y a tu pequeña tienda!\n{Susurro} Solo volveré a casa y lloraré ahora.\n{Gritando} ¿Por qué yo?!", + ) + + def make_insert_speech_type_fn(index): + def insert_speech_type_fn(current_text, speech_type_name): + current_text = current_text or "" + speech_type_name = speech_type_name or "Ninguno" + updated_text = current_text + f"{{{speech_type_name}}} " + return gr.update(value=updated_text) + + return insert_speech_type_fn + + for i, insert_btn in enumerate(speech_type_insert_btns): + insert_fn = make_insert_speech_type_fn(i) + insert_btn.click( + insert_fn, + inputs=[gen_text_input_multistyle, speech_type_names[i]], + outputs=gen_text_input_multistyle, + ) + + # Model choice + model_choice_multistyle = gr.Radio(choices=["F5-TTS"], label="Seleccionar Modelo TTS", value="F5-TTS") + + with gr.Accordion("Configuraciones Avanzadas", open=False): + remove_silence_multistyle = gr.Checkbox( + label="Eliminar Silencios", + value=False, + ) + + # Generate button + generate_multistyle_btn = gr.Button("Generar Habla Multi-Estilo", variant="primary") + + # Output audio + audio_output_multistyle = gr.Audio(label="Audio Sintetizado") + + @gpu_decorator + def generate_multistyle_speech( + regular_audio, + regular_ref_text, + gen_text, + *args, + ): + num_additional_speech_types = max_speech_types - 1 + speech_type_names_list = args[:num_additional_speech_types] + speech_type_audios_list = args[num_additional_speech_types : 2 * num_additional_speech_types] + speech_type_ref_texts_list = args[2 * num_additional_speech_types : 3 * num_additional_speech_types] + model_choice = args[3 * num_additional_speech_types] + remove_silence = args[3 * num_additional_speech_types + 1] + + # Collect the speech types and their audios into a dict + speech_types = {"Regular": {"audio": regular_audio, "ref_text": regular_ref_text}} + + for name_input, audio_input, ref_text_input in zip( + speech_type_names_list, speech_type_audios_list, speech_type_ref_texts_list + ): + if name_input and audio_input: + speech_types[name_input] = {"audio": audio_input, "ref_text": ref_text_input} + + # Parse the gen_text into segments + segments = parse_speechtypes_text(gen_text) + + # For each segment, generate speech + generated_audio_segments = [] + current_style = "Regular" + + for segment in segments: + style = segment["style"] + text = segment["text"] + + if style in speech_types: + current_style = style + else: + # If style not available, default to Regular + current_style = "Regular" + + ref_audio = speech_types[current_style]["audio"] + ref_text = speech_types[current_style].get("ref_text", "") + + # Generate speech for this segment + audio, _ = infer( + ref_audio, ref_text, text, model_choice, remove_silence, 0, show_info=print + ) # show_info=print no pull to top when generating + sr, audio_data = audio + + generated_audio_segments.append(audio_data) + + # Concatenate all audio segments + if generated_audio_segments: + final_audio_data = np.concatenate(generated_audio_segments) + return (sr, final_audio_data) + else: + gr.Warning("No se generó ningún audio.") + return None + + generate_multistyle_btn.click( + generate_multistyle_speech, + inputs=[ + regular_audio, + regular_ref_text, + gen_text_input_multistyle, + ] + + speech_type_names + + speech_type_audios + + speech_type_ref_texts + + [ + model_choice_multistyle, + remove_silence_multistyle, + ], + outputs=audio_output_multistyle, + ) + + # Validation function to disable Generate button if speech types are missing + def validate_speech_types(gen_text, regular_name, *args): + num_additional_speech_types = max_speech_types - 1 + speech_type_names_list = args[:num_additional_speech_types] + + # Collect the speech types names + speech_types_available = set() + if regular_name: + speech_types_available.add(regular_name) + for name_input in speech_type_names_list: + if name_input: + speech_types_available.add(name_input) + + # Parse the gen_text to get the speech types used + segments = parse_speechtypes_text(gen_text) + speech_types_in_text = set(segment["style"] for segment in segments) + + # Check if all speech types in text are available + missing_speech_types = speech_types_in_text - speech_types_available + + if missing_speech_types: + # Disable the generate button + return gr.update(interactive=False) + else: + # Enable the generate button + return gr.update(interactive=True) + + gen_text_input_multistyle.change( + validate_speech_types, + inputs=[gen_text_input_multistyle, regular_name] + speech_type_names, + outputs=generate_multistyle_btn, + ) + + +with gr.Blocks() as app_chat: + gr.Markdown( + """ +# Chat de Voz +¡Mantén una conversación con una IA usando tu voz de referencia! +1. Sube un clip de audio de referencia y opcionalmente su transcripción. +2. Carga el modelo de chat. +3. Graba tu mensaje a través de tu micrófono. +4. La IA responderá usando la voz de referencia. +""" + ) + + if not USING_SPACES: + load_chat_model_btn = gr.Button("Cargar Modelo de Chat", variant="primary") + + chat_interface_container = gr.Column(visible=False) + + @gpu_decorator + def load_chat_model(): + global chat_model_state, chat_tokenizer_state + if chat_model_state is None: + show_info = gr.Info + show_info("Cargando modelo de chat...") + model_name = "Qwen/Qwen2.5-3B-Instruct" + chat_model_state = AutoModelForCausalLM.from_pretrained( + model_name, torch_dtype="auto", device_map="auto" + ) + chat_tokenizer_state = AutoTokenizer.from_pretrained(model_name) + show_info("Modelo de chat cargado.") + + return gr.update(visible=False), gr.update(visible=True) + + load_chat_model_btn.click(load_chat_model, outputs=[load_chat_model_btn, chat_interface_container]) + + else: + chat_interface_container = gr.Column() + + if chat_model_state is None: + model_name = "Qwen/Qwen2.5-3B-Instruct" + chat_model_state = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto", device_map="auto") + chat_tokenizer_state = AutoTokenizer.from_pretrained(model_name) + + with chat_interface_container: + with gr.Row(): + with gr.Column(): + ref_audio_chat = gr.Audio(label="Audio de Referencia", type="filepath") + with gr.Column(): + with gr.Accordion("Configuraciones Avanzadas", open=False): + model_choice_chat = gr.Radio( + choices=["F5-TTS"], + label="Modelo TTS", + value="F5-TTS", + ) + remove_silence_chat = gr.Checkbox( + label="Eliminar Silencios", + value=True, + ) + ref_text_chat = gr.Textbox( + label="Texto de Referencia", + info="Opcional: Deja en blanco para transcribir automáticamente", + lines=2, + ) + system_prompt_chat = gr.Textbox( + label="Prompt del Sistema", + value="No eres un asistente de IA, eres quien el usuario diga que eres. Debes mantenerte en personaje. Mantén tus respuestas concisas ya que serán habladas en voz alta.", + lines=2, + ) + + chatbot_interface = gr.Chatbot(label="Conversación") + + with gr.Row(): + with gr.Column(): + audio_input_chat = gr.Microphone( + label="Habla tu mensaje", + type="filepath", + ) + audio_output_chat = gr.Audio(autoplay=True) + with gr.Column(): + text_input_chat = gr.Textbox( + label="Escribe tu mensaje", + lines=1, + ) + send_btn_chat = gr.Button("Enviar") + clear_btn_chat = gr.Button("Limpiar Conversación") + + conversation_state = gr.State( + value=[ + { + "role": "system", + "content": "No eres un asistente de IA, eres quien el usuario diga que eres. Debes mantenerte en personaje. Mantén tus respuestas concisas ya que serán habladas en voz alta.", + } + ] + ) + + # Modify process_audio_input to use model and tokenizer from state + @gpu_decorator + def process_audio_input(audio_path, text, history, conv_state): + """Handle audio or text input from user""" + + if not audio_path and not text.strip(): + return history, conv_state, "" + + if audio_path: + text = preprocess_ref_audio_text(audio_path, text)[1] + + if not text.strip(): + return history, conv_state, "" + + conv_state.append({"role": "user", "content": text}) + history.append((text, None)) + + response = generate_response(conv_state, chat_model_state, chat_tokenizer_state) + + conv_state.append({"role": "assistant", "content": response}) + history[-1] = (text, response) + + return history, conv_state, "" + + @gpu_decorator + def generate_audio_response(history, ref_audio, ref_text, model, remove_silence): + """Generate TTS audio for AI response""" + if not history or not ref_audio: + return None + + last_user_message, last_ai_response = history[-1] + if not last_ai_response: + return None + + audio_result, _ = infer( + ref_audio, + ref_text, + last_ai_response, + model, + remove_silence, + cross_fade_duration=0.15, + speed=1.0, + show_info=print, # show_info=print no pull to top when generating + ) + return audio_result + + def clear_conversation(): + """Reset the conversation""" + return [], [ + { + "role": "system", + "content": "No eres un asistente de IA, eres quien el usuario diga que eres. Debes mantenerte en personaje. Mantén tus respuestas concisas ya que serán habladas en voz alta.", + } + ] + + def update_system_prompt(new_prompt): + """Update the system prompt and reset the conversation""" + new_conv_state = [{"role": "system", "content": new_prompt}] + return [], new_conv_state + + # Handle audio input + audio_input_chat.stop_recording( + process_audio_input, + inputs=[audio_input_chat, text_input_chat, chatbot_interface, conversation_state], + outputs=[chatbot_interface, conversation_state], + ).then( + generate_audio_response, + inputs=[chatbot_interface, ref_audio_chat, ref_text_chat, model_choice_chat, remove_silence_chat], + outputs=[audio_output_chat], + ).then( + lambda: None, + None, + audio_input_chat, + ) + + # Handle text input + text_input_chat.submit( + process_audio_input, + inputs=[audio_input_chat, text_input_chat, chatbot_interface, conversation_state], + outputs=[chatbot_interface, conversation_state], + ).then( + generate_audio_response, + inputs=[chatbot_interface, ref_audio_chat, ref_text_chat, model_choice_chat, remove_silence_chat], + outputs=[audio_output_chat], + ).then( + lambda: None, + None, + text_input_chat, + ) + + # Handle send button + send_btn_chat.click( + process_audio_input, + inputs=[audio_input_chat, text_input_chat, chatbot_interface, conversation_state], + outputs=[chatbot_interface, conversation_state], + ).then( + generate_audio_response, + inputs=[chatbot_interface, ref_audio_chat, ref_text_chat, model_choice_chat, remove_silence_chat], + outputs=[audio_output_chat], + ).then( + lambda: None, + None, + text_input_chat, + ) + + # Handle clear button + clear_btn_chat.click( + clear_conversation, + outputs=[chatbot_interface, conversation_state], + ) + + # Handle system prompt change and reset conversation + system_prompt_chat.change( + update_system_prompt, + inputs=system_prompt_chat, + outputs=[chatbot_interface, conversation_state], + ) + + +with gr.Blocks() as app: + gr.Markdown( + """ +# Spanish-F5 + +Esta es una interfaz web para F5 TTS, con un finetuning para poder hablar en castellano + +Implementación original: +* [F5-TTS](https://arxiv.org/abs/2410.06885) (A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching) + +El modelo sólo soporta el castellano. + +Para los mejores resultados, intenta convertir tu audio de referencia a WAV o MP3, asegurarte de que duren entre 11 y 14 segundos, que comiencen y acaben con entre medio segundo y un segundo de silencio, y a ser posible que acabe con el final de la frase. + +**NOTA: El texto de referencia será transcrito automáticamente con Whisper si no se proporciona. Para mejores resultados, mantén tus clips de referencia cortos (<15s). Asegúrate de que el audio esté completamente subido antes de generar. Se utiliza la librería num2words para convertir los números a palabras.** +""" + ) + gr.TabbedInterface( + [app_tts, app_multistyle, app_chat, app_credits], + ["TTS", "Multi-Habla", "Chat de Voz", "Créditos"], + ) + + +@click.command() +@click.option("--port", "-p", default=None, type=int, help="Puerto para ejecutar la aplicación") +@click.option("--host", "-H", default=None, help="Host para ejecutar la aplicación") +@click.option( + "--share", + "-s", + default=False, + is_flag=True, + help="Compartir la aplicación a través de un enlace compartido de Gradio", +) +@click.option("--api", "-a", default=True, is_flag=True, help="Permitir acceso a la API") +def main(port, host, share, api): + global app + print("Iniciando la aplicación...") + app.queue(api_open=api).launch(server_name=host, server_port=port, share=True, show_api=api) + + +if __name__ == "__main__": + if not USING_SPACES: + main() + else: + app.queue().launch() diff --git a/src/f5_tts/infer/speech_edit.py b/src/f5_tts/infer/speech_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..596de8e412935ece8d2f191be2ba14ab9805fa88 --- /dev/null +++ b/src/f5_tts/infer/speech_edit.py @@ -0,0 +1,191 @@ +import os + +import torch +import torch.nn.functional as F +import torchaudio + +from f5_tts.infer.utils_infer import load_checkpoint, load_vocoder, save_spectrogram +from f5_tts.model import CFM, DiT, UNetT +from f5_tts.model.utils import convert_char_to_pinyin, get_tokenizer + +device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" + + +# --------------------- Dataset Settings -------------------- # + +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 +win_length = 1024 +n_fft = 1024 +mel_spec_type = "vocos" # 'vocos' or 'bigvgan' +target_rms = 0.1 + +tokenizer = "pinyin" +dataset_name = "Emilia_ZH_EN" + + +# ---------------------- infer setting ---------------------- # + +seed = None # int | None + +exp_name = "F5TTS_Base" # F5TTS_Base | E2TTS_Base +ckpt_step = 1200000 + +nfe_step = 32 # 16, 32 +cfg_strength = 2.0 +ode_method = "euler" # euler | midpoint +sway_sampling_coef = -1.0 +speed = 1.0 + +if exp_name == "F5TTS_Base": + model_cls = DiT + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + +elif exp_name == "E2TTS_Base": + model_cls = UNetT + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + +ckpt_path = f"ckpts/{exp_name}/model_{ckpt_step}.safetensors" +output_dir = "tests" + +# [leverage https://github.com/MahmoudAshraf97/ctc-forced-aligner to get char level alignment] +# pip install git+https://github.com/MahmoudAshraf97/ctc-forced-aligner.git +# [write the origin_text into a file, e.g. tests/test_edit.txt] +# ctc-forced-aligner --audio_path "src/f5_tts/infer/examples/basic/basic_ref_en.wav" --text_path "tests/test_edit.txt" --language "zho" --romanize --split_size "char" +# [result will be saved at same path of audio file] +# [--language "zho" for Chinese, "eng" for English] +# [if local ckpt, set --alignment_model "../checkpoints/mms-300m-1130-forced-aligner"] + +audio_to_edit = "src/f5_tts/infer/examples/basic/basic_ref_en.wav" +origin_text = "Some call me nature, others call me mother nature." +target_text = "Some call me optimist, others call me realist." +parts_to_edit = [ + [1.42, 2.44], + [4.04, 4.9], +] # stard_ends of "nature" & "mother nature", in seconds +fix_duration = [ + 1.2, + 1, +] # fix duration for "optimist" & "realist", in seconds + +# audio_to_edit = "src/f5_tts/infer/examples/basic/basic_ref_zh.wav" +# origin_text = "对,这就是我,万人敬仰的太乙真人。" +# target_text = "对,那就是你,万人敬仰的太白金星。" +# parts_to_edit = [[0.84, 1.4], [1.92, 2.4], [4.26, 6.26], ] +# fix_duration = None # use origin text duration + + +# -------------------------------------------------# + +use_ema = True + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + +# Vocoder model +local = False +if mel_spec_type == "vocos": + vocoder_local_path = "../checkpoints/charactr/vocos-mel-24khz" +elif mel_spec_type == "bigvgan": + vocoder_local_path = "../checkpoints/bigvgan_v2_24khz_100band_256x" +vocoder = load_vocoder(vocoder_name=mel_spec_type, is_local=local, local_path=vocoder_local_path) + +# Tokenizer +vocab_char_map, vocab_size = get_tokenizer(dataset_name, tokenizer) + +# Model +model = CFM( + transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels), + mel_spec_kwargs=dict( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ), + odeint_kwargs=dict( + method=ode_method, + ), + vocab_char_map=vocab_char_map, +).to(device) + +dtype = torch.float32 if mel_spec_type == "bigvgan" else None +model = load_checkpoint(model, ckpt_path, device, dtype=dtype, use_ema=use_ema) + +# Audio +audio, sr = torchaudio.load(audio_to_edit) +if audio.shape[0] > 1: + audio = torch.mean(audio, dim=0, keepdim=True) +rms = torch.sqrt(torch.mean(torch.square(audio))) +if rms < target_rms: + audio = audio * target_rms / rms +if sr != target_sample_rate: + resampler = torchaudio.transforms.Resample(sr, target_sample_rate) + audio = resampler(audio) +offset = 0 +audio_ = torch.zeros(1, 0) +edit_mask = torch.zeros(1, 0, dtype=torch.bool) +for part in parts_to_edit: + start, end = part + part_dur = end - start if fix_duration is None else fix_duration.pop(0) + part_dur = part_dur * target_sample_rate + start = start * target_sample_rate + audio_ = torch.cat((audio_, audio[:, round(offset) : round(start)], torch.zeros(1, round(part_dur))), dim=-1) + edit_mask = torch.cat( + ( + edit_mask, + torch.ones(1, round((start - offset) / hop_length), dtype=torch.bool), + torch.zeros(1, round(part_dur / hop_length), dtype=torch.bool), + ), + dim=-1, + ) + offset = end * target_sample_rate +# audio = torch.cat((audio_, audio[:, round(offset):]), dim = -1) +edit_mask = F.pad(edit_mask, (0, audio.shape[-1] // hop_length - edit_mask.shape[-1] + 1), value=True) +audio = audio.to(device) +edit_mask = edit_mask.to(device) + +# Text +text_list = [target_text] +if tokenizer == "pinyin": + final_text_list = convert_char_to_pinyin(text_list) +else: + final_text_list = [text_list] +print(f"text : {text_list}") +print(f"pinyin: {final_text_list}") + +# Duration +ref_audio_len = 0 +duration = audio.shape[-1] // hop_length + +# Inference +with torch.inference_mode(): + generated, trajectory = model.sample( + cond=audio, + text=final_text_list, + duration=duration, + steps=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + seed=seed, + edit_mask=edit_mask, + ) + print(f"Generated mel: {generated.shape}") + + # Final result + generated = generated.to(torch.float32) + generated = generated[:, ref_audio_len:, :] + gen_mel_spec = generated.permute(0, 2, 1) + if mel_spec_type == "vocos": + generated_wave = vocoder.decode(gen_mel_spec) + elif mel_spec_type == "bigvgan": + generated_wave = vocoder(gen_mel_spec) + + if rms < target_rms: + generated_wave = generated_wave * rms / target_rms + + save_spectrogram(gen_mel_spec[0].cpu().numpy(), f"{output_dir}/speech_edit_out.png") + torchaudio.save(f"{output_dir}/speech_edit_out.wav", generated_wave.squeeze(0).cpu(), target_sample_rate) + print(f"Generated wav: {generated_wave.shape}") diff --git a/src/f5_tts/infer/utils_infer.py b/src/f5_tts/infer/utils_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..b81eb8b584e4d72be219e4cd5d480180caf665a3 --- /dev/null +++ b/src/f5_tts/infer/utils_infer.py @@ -0,0 +1,511 @@ +# A unified script for inference process +# Make adjustments inside functions, and consider both gradio and cli scripts if need to change func output format +import os +import sys + +sys.path.append(f"../../{os.path.dirname(os.path.abspath(__file__))}/third_party/BigVGAN/") + +import hashlib +import re +import tempfile +from importlib.resources import files + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pylab as plt +import numpy as np +import torch +import torchaudio +import tqdm +from pydub import AudioSegment, silence +from transformers import pipeline +from vocos import Vocos + +from f5_tts.model import CFM +from f5_tts.model.utils import ( + get_tokenizer, + convert_char_to_pinyin, +) + +_ref_audio_cache = {} + +device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" + +# ----------------------------------------- + +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 +win_length = 1024 +n_fft = 1024 +mel_spec_type = "vocos" +target_rms = 0.1 +cross_fade_duration = 0.15 +ode_method = "euler" +nfe_step = 32 # 16, 32 +cfg_strength = 2.0 +sway_sampling_coef = -1.0 +speed = 1.0 +fix_duration = None + +# ----------------------------------------- + + +# chunk text into smaller pieces + + +def chunk_text(text, max_chars=135): + """ + Splits the input text into chunks, each with a maximum number of characters. + + Args: + text (str): The text to be split. + max_chars (int): The maximum number of characters per chunk. + + Returns: + List[str]: A list of text chunks. + """ + chunks = [] + current_chunk = "" + # Split the text into sentences based on punctuation followed by whitespace + sentences = re.split(r"(?<=[;:,.!?])\s+|(?<=[;:,。!?])", text) + + for sentence in sentences: + if len(current_chunk.encode("utf-8")) + len(sentence.encode("utf-8")) <= max_chars: + current_chunk += sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence + else: + if current_chunk: + chunks.append(current_chunk.strip()) + current_chunk = sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence + + if current_chunk: + chunks.append(current_chunk.strip()) + + return chunks + + +# load vocoder +def load_vocoder(vocoder_name="vocos", is_local=False, local_path="", device=device): + if vocoder_name == "vocos": + if is_local: + print(f"Load vocos from local path {local_path}") + vocoder = Vocos.from_hparams(f"{local_path}/config.yaml") + state_dict = torch.load(f"{local_path}/pytorch_model.bin", map_location="cpu") + vocoder.load_state_dict(state_dict) + vocoder = vocoder.eval().to(device) + else: + print("Download Vocos from huggingface charactr/vocos-mel-24khz") + vocoder = Vocos.from_pretrained("charactr/vocos-mel-24khz").to(device) + elif vocoder_name == "bigvgan": + try: + from third_party.BigVGAN import bigvgan + except ImportError: + print("You need to follow the README to init submodule and change the BigVGAN source code.") + if is_local: + """download from https://huggingface.co/nvidia/bigvgan_v2_24khz_100band_256x/tree/main""" + vocoder = bigvgan.BigVGAN.from_pretrained(local_path, use_cuda_kernel=False) + else: + vocoder = bigvgan.BigVGAN.from_pretrained("nvidia/bigvgan_v2_24khz_100band_256x", use_cuda_kernel=False) + + vocoder.remove_weight_norm() + vocoder = vocoder.eval().to(device) + return vocoder + + +# load asr pipeline + +asr_pipe = None + + +def initialize_asr_pipeline(device=device, dtype=None): + if dtype is None: + dtype = ( + torch.float16 if device == "cuda" and torch.cuda.get_device_properties(device).major >= 6 else torch.float32 + ) + global asr_pipe + asr_pipe = pipeline( + "automatic-speech-recognition", + model="openai/whisper-large-v3-turbo", + torch_dtype=dtype, + device=device, + ) + + +# load model checkpoint for inference + + +def load_checkpoint(model, ckpt_path, device, dtype=None, use_ema=True): + if dtype is None: + dtype = ( + torch.float16 if device == "cuda" and torch.cuda.get_device_properties(device).major >= 6 else torch.float32 + ) + model = model.to(dtype) + + ckpt_type = ckpt_path.split(".")[-1] + if ckpt_type == "safetensors": + from safetensors.torch import load_file + + checkpoint = load_file(ckpt_path) + else: + checkpoint = torch.load(ckpt_path, weights_only=True) + + if use_ema: + if ckpt_type == "safetensors": + checkpoint = {"ema_model_state_dict": checkpoint} + checkpoint["model_state_dict"] = { + k.replace("ema_model.", ""): v + for k, v in checkpoint["ema_model_state_dict"].items() + if k not in ["initted", "step"] + } + + # patch for backward compatibility, 305e3ea + for key in ["mel_spec.mel_stft.mel_scale.fb", "mel_spec.mel_stft.spectrogram.window"]: + if key in checkpoint["model_state_dict"]: + del checkpoint["model_state_dict"][key] + + model.load_state_dict(checkpoint["model_state_dict"]) + else: + if ckpt_type == "safetensors": + checkpoint = {"model_state_dict": checkpoint} + model.load_state_dict(checkpoint["model_state_dict"]) + + return model.to(device) + + +# load model for inference + + +def load_model( + model_cls, + model_cfg, + ckpt_path, + mel_spec_type=mel_spec_type, + vocab_file="", + ode_method=ode_method, + use_ema=True, + device=device, +): + if vocab_file == "": + vocab_file = str(files("f5_tts").joinpath("infer/examples/vocab.txt")) + tokenizer = "custom" + + print("\nvocab : ", vocab_file) + print("tokenizer : ", tokenizer) + print("model : ", ckpt_path, "\n") + + vocab_char_map, vocab_size = get_tokenizer(vocab_file, tokenizer) + model = CFM( + transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels), + mel_spec_kwargs=dict( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ), + odeint_kwargs=dict( + method=ode_method, + ), + vocab_char_map=vocab_char_map, + ).to(device) + + dtype = torch.float32 if mel_spec_type == "bigvgan" else None + model = load_checkpoint(model, ckpt_path, device, dtype=dtype, use_ema=use_ema) + + return model + + +def remove_silence_edges(audio, silence_threshold=-42): + # Remove silence from the start + non_silent_start_idx = silence.detect_leading_silence(audio, silence_threshold=silence_threshold) + audio = audio[non_silent_start_idx:] + + # Remove silence from the end + non_silent_end_duration = audio.duration_seconds + for ms in reversed(audio): + if ms.dBFS > silence_threshold: + break + non_silent_end_duration -= 0.001 + trimmed_audio = audio[: int(non_silent_end_duration * 1000)] + + return trimmed_audio + + +# preprocess reference audio and text + + +def preprocess_ref_audio_text(ref_audio_orig, ref_text, clip_short=True, show_info=print, device=device): + show_info("Converting audio...") + with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f: + aseg = AudioSegment.from_file(ref_audio_orig) + + if clip_short: + # 1. try to find long silence for clipping + non_silent_segs = silence.split_on_silence( + aseg, min_silence_len=1000, silence_thresh=-50, keep_silence=1000, seek_step=10 + ) + non_silent_wave = AudioSegment.silent(duration=0) + for non_silent_seg in non_silent_segs: + if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 15000: + show_info("Audio is over 15s, clipping short. (1)") + break + non_silent_wave += non_silent_seg + + # 2. try to find short silence for clipping if 1. failed + if len(non_silent_wave) > 15000: + non_silent_segs = silence.split_on_silence( + aseg, min_silence_len=100, silence_thresh=-40, keep_silence=1000, seek_step=10 + ) + non_silent_wave = AudioSegment.silent(duration=0) + for non_silent_seg in non_silent_segs: + if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 15000: + show_info("Audio is over 15s, clipping short. (2)") + break + non_silent_wave += non_silent_seg + + aseg = non_silent_wave + + # 3. if no proper silence found for clipping + if len(aseg) > 15000: + aseg = aseg[:15000] + show_info("Audio is over 15s, clipping short. (3)") + + aseg = remove_silence_edges(aseg) + AudioSegment.silent(duration=50) + aseg.export(f.name, format="wav") + ref_audio = f.name + + # Compute a hash of the reference audio file + with open(ref_audio, "rb") as audio_file: + audio_data = audio_file.read() + audio_hash = hashlib.md5(audio_data).hexdigest() + + global _ref_audio_cache + if audio_hash in _ref_audio_cache: + # Use cached reference text + show_info("Using cached reference text...") + ref_text = _ref_audio_cache[audio_hash] + else: + if not ref_text.strip(): + global asr_pipe + if asr_pipe is None: + initialize_asr_pipeline(device=device) + show_info("No reference text provided, transcribing reference audio...") + ref_text = asr_pipe( + ref_audio, + chunk_length_s=30, + batch_size=128, + generate_kwargs={"task": "transcribe"}, + return_timestamps=False, + )["text"].strip() + show_info("Finished transcription") + else: + show_info("Using custom reference text...") + # Cache the transcribed text + _ref_audio_cache[audio_hash] = ref_text + + # Ensure ref_text ends with a proper sentence-ending punctuation + if not ref_text.endswith(". ") and not ref_text.endswith("。"): + if ref_text.endswith("."): + ref_text += " " + else: + ref_text += ". " + + return ref_audio, ref_text + + +# infer process: chunk text -> infer batches [i.e. infer_batch_process()] + + +def infer_process( + ref_audio, + ref_text, + gen_text, + model_obj, + vocoder, + mel_spec_type=mel_spec_type, + show_info=print, + progress=tqdm, + target_rms=target_rms, + cross_fade_duration=cross_fade_duration, + nfe_step=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + speed=speed, + fix_duration=fix_duration, + device=device, +): + # Split the input text into batches + audio, sr = torchaudio.load(ref_audio) + max_chars = int(len(ref_text.encode("utf-8")) / (audio.shape[-1] / sr) * (25 - audio.shape[-1] / sr)) + gen_text_batches = chunk_text(gen_text, max_chars=max_chars) + for i, gen_text in enumerate(gen_text_batches): + print(f"gen_text {i}", gen_text) + + show_info(f"Generating audio in {len(gen_text_batches)} batches...") + return infer_batch_process( + (audio, sr), + ref_text, + gen_text_batches, + model_obj, + vocoder, + mel_spec_type=mel_spec_type, + progress=progress, + target_rms=target_rms, + cross_fade_duration=cross_fade_duration, + nfe_step=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + speed=speed, + fix_duration=fix_duration, + device=device, + ) + + +# infer batches + + +def infer_batch_process( + ref_audio, + ref_text, + gen_text_batches, + model_obj, + vocoder, + mel_spec_type="vocos", + progress=tqdm, + target_rms=0.1, + cross_fade_duration=0.15, + nfe_step=32, + cfg_strength=2.0, + sway_sampling_coef=-1, + speed=1, + fix_duration=None, + device=None, +): + audio, sr = ref_audio + if audio.shape[0] > 1: + audio = torch.mean(audio, dim=0, keepdim=True) + + rms = torch.sqrt(torch.mean(torch.square(audio))) + if rms < target_rms: + audio = audio * target_rms / rms + if sr != target_sample_rate: + resampler = torchaudio.transforms.Resample(sr, target_sample_rate) + audio = resampler(audio) + audio = audio.to(device) + + generated_waves = [] + spectrograms = [] + + if len(ref_text[-1].encode("utf-8")) == 1: + ref_text = ref_text + " " + for i, gen_text in enumerate(progress.tqdm(gen_text_batches)): + # Prepare the text + text_list = [ref_text + gen_text] + final_text_list = convert_char_to_pinyin(text_list) + + ref_audio_len = audio.shape[-1] // hop_length + if fix_duration is not None: + duration = int(fix_duration * target_sample_rate / hop_length) + else: + # Calculate duration + ref_text_len = len(ref_text.encode("utf-8")) + gen_text_len = len(gen_text.encode("utf-8")) + duration = ref_audio_len + int(ref_audio_len / ref_text_len * gen_text_len / speed) + + # inference + with torch.inference_mode(): + generated, _ = model_obj.sample( + cond=audio, + text=final_text_list, + duration=duration, + steps=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + ) + + generated = generated.to(torch.float32) + generated = generated[:, ref_audio_len:, :] + generated_mel_spec = generated.permute(0, 2, 1) + if mel_spec_type == "vocos": + generated_wave = vocoder.decode(generated_mel_spec) + elif mel_spec_type == "bigvgan": + generated_wave = vocoder(generated_mel_spec) + if rms < target_rms: + generated_wave = generated_wave * rms / target_rms + + # wav -> numpy + generated_wave = generated_wave.squeeze().cpu().numpy() + + generated_waves.append(generated_wave) + spectrograms.append(generated_mel_spec[0].cpu().numpy()) + + # Combine all generated waves with cross-fading + if cross_fade_duration <= 0: + # Simply concatenate + final_wave = np.concatenate(generated_waves) + else: + final_wave = generated_waves[0] + for i in range(1, len(generated_waves)): + prev_wave = final_wave + next_wave = generated_waves[i] + + # Calculate cross-fade samples, ensuring it does not exceed wave lengths + cross_fade_samples = int(cross_fade_duration * target_sample_rate) + cross_fade_samples = min(cross_fade_samples, len(prev_wave), len(next_wave)) + + if cross_fade_samples <= 0: + # No overlap possible, concatenate + final_wave = np.concatenate([prev_wave, next_wave]) + continue + + # Overlapping parts + prev_overlap = prev_wave[-cross_fade_samples:] + next_overlap = next_wave[:cross_fade_samples] + + # Fade out and fade in + fade_out = np.linspace(1, 0, cross_fade_samples) + fade_in = np.linspace(0, 1, cross_fade_samples) + + # Cross-faded overlap + cross_faded_overlap = prev_overlap * fade_out + next_overlap * fade_in + + # Combine + new_wave = np.concatenate( + [prev_wave[:-cross_fade_samples], cross_faded_overlap, next_wave[cross_fade_samples:]] + ) + + final_wave = new_wave + + # Create a combined spectrogram + combined_spectrogram = np.concatenate(spectrograms, axis=1) + + return final_wave, target_sample_rate, combined_spectrogram + + +# remove silence from generated wav + + +def remove_silence_for_generated_wav(filename): + aseg = AudioSegment.from_file(filename) + non_silent_segs = silence.split_on_silence( + aseg, min_silence_len=1000, silence_thresh=-50, keep_silence=500, seek_step=10 + ) + non_silent_wave = AudioSegment.silent(duration=0) + for non_silent_seg in non_silent_segs: + non_silent_wave += non_silent_seg + aseg = non_silent_wave + aseg.export(filename, format="wav") + + +# save spectrogram + + +def save_spectrogram(spectrogram, path): + plt.figure(figsize=(12, 4)) + plt.imshow(spectrogram, origin="lower", aspect="auto") + plt.colorbar() + plt.savefig(path) + plt.close() diff --git a/src/f5_tts/model/__init__.py b/src/f5_tts/model/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1568538bd93def9204fc9954c67d162dbded0ac --- /dev/null +++ b/src/f5_tts/model/__init__.py @@ -0,0 +1,10 @@ +from f5_tts.model.cfm import CFM + +from f5_tts.model.backbones.unett import UNetT +from f5_tts.model.backbones.dit import DiT +from f5_tts.model.backbones.mmdit import MMDiT + +from f5_tts.model.trainer import Trainer + + +__all__ = ["CFM", "UNetT", "DiT", "MMDiT", "Trainer"] diff --git a/src/f5_tts/model/backbones/README.md b/src/f5_tts/model/backbones/README.md new file mode 100644 index 0000000000000000000000000000000000000000..513a0548bbdcb87f5763357b5c228b9345080e14 --- /dev/null +++ b/src/f5_tts/model/backbones/README.md @@ -0,0 +1,20 @@ +## Backbones quick introduction + + +### unett.py +- flat unet transformer +- structure same as in e2-tts & voicebox paper except using rotary pos emb +- update: allow possible abs pos emb & convnextv2 blocks for embedded text before concat + +### dit.py +- adaln-zero dit +- embedded timestep as condition +- concatted noised_input + masked_cond + embedded_text, linear proj in +- possible abs pos emb & convnextv2 blocks for embedded text before concat +- possible long skip connection (first layer to last layer) + +### mmdit.py +- sd3 structure +- timestep as condition +- left stream: text embedded and applied a abs pos emb +- right stream: masked_cond & noised_input concatted and with same conv pos emb as unett diff --git a/src/f5_tts/model/backbones/dit.py b/src/f5_tts/model/backbones/dit.py new file mode 100644 index 0000000000000000000000000000000000000000..f59fa57ab757bb15c8f67815440ba9545b2313d2 --- /dev/null +++ b/src/f5_tts/model/backbones/dit.py @@ -0,0 +1,163 @@ +""" +ein notation: +b - batch +n - sequence +nt - text sequence +nw - raw wave length +d - dimension +""" + +from __future__ import annotations + +import torch +from torch import nn +import torch.nn.functional as F + +from x_transformers.x_transformers import RotaryEmbedding + +from f5_tts.model.modules import ( + TimestepEmbedding, + ConvNeXtV2Block, + ConvPositionEmbedding, + DiTBlock, + AdaLayerNormZero_Final, + precompute_freqs_cis, + get_pos_embed_indices, +) + + +# Text embedding + + +class TextEmbedding(nn.Module): + def __init__(self, text_num_embeds, text_dim, conv_layers=0, conv_mult=2): + super().__init__() + self.text_embed = nn.Embedding(text_num_embeds + 1, text_dim) # use 0 as filler token + + if conv_layers > 0: + self.extra_modeling = True + self.precompute_max_pos = 4096 # ~44s of 24khz audio + self.register_buffer("freqs_cis", precompute_freqs_cis(text_dim, self.precompute_max_pos), persistent=False) + self.text_blocks = nn.Sequential( + *[ConvNeXtV2Block(text_dim, text_dim * conv_mult) for _ in range(conv_layers)] + ) + else: + self.extra_modeling = False + + def forward(self, text: int["b nt"], seq_len, drop_text=False): # noqa: F722 + text = text + 1 # use 0 as filler token. preprocess of batch pad -1, see list_str_to_idx() + text = text[:, :seq_len] # curtail if character tokens are more than the mel spec tokens + batch, text_len = text.shape[0], text.shape[1] + text = F.pad(text, (0, seq_len - text_len), value=0) + + if drop_text: # cfg for text + text = torch.zeros_like(text) + + text = self.text_embed(text) # b n -> b n d + + # possible extra modeling + if self.extra_modeling: + # sinus pos emb + batch_start = torch.zeros((batch,), dtype=torch.long) + pos_idx = get_pos_embed_indices(batch_start, seq_len, max_pos=self.precompute_max_pos) + text_pos_embed = self.freqs_cis[pos_idx] + text = text + text_pos_embed + + # convnextv2 blocks + text = self.text_blocks(text) + + return text + + +# noised input audio and context mixing embedding + + +class InputEmbedding(nn.Module): + def __init__(self, mel_dim, text_dim, out_dim): + super().__init__() + self.proj = nn.Linear(mel_dim * 2 + text_dim, out_dim) + self.conv_pos_embed = ConvPositionEmbedding(dim=out_dim) + + def forward(self, x: float["b n d"], cond: float["b n d"], text_embed: float["b n d"], drop_audio_cond=False): # noqa: F722 + if drop_audio_cond: # cfg for cond audio + cond = torch.zeros_like(cond) + + x = self.proj(torch.cat((x, cond, text_embed), dim=-1)) + x = self.conv_pos_embed(x) + x + return x + + +# Transformer backbone using DiT blocks + + +class DiT(nn.Module): + def __init__( + self, + *, + dim, + depth=8, + heads=8, + dim_head=64, + dropout=0.1, + ff_mult=4, + mel_dim=100, + text_num_embeds=256, + text_dim=None, + conv_layers=0, + long_skip_connection=False, + ): + super().__init__() + + self.time_embed = TimestepEmbedding(dim) + if text_dim is None: + text_dim = mel_dim + self.text_embed = TextEmbedding(text_num_embeds, text_dim, conv_layers=conv_layers) + self.input_embed = InputEmbedding(mel_dim, text_dim, dim) + + self.rotary_embed = RotaryEmbedding(dim_head) + + self.dim = dim + self.depth = depth + + self.transformer_blocks = nn.ModuleList( + [DiTBlock(dim=dim, heads=heads, dim_head=dim_head, ff_mult=ff_mult, dropout=dropout) for _ in range(depth)] + ) + self.long_skip_connection = nn.Linear(dim * 2, dim, bias=False) if long_skip_connection else None + + self.norm_out = AdaLayerNormZero_Final(dim) # final modulation + self.proj_out = nn.Linear(dim, mel_dim) + + def forward( + self, + x: float["b n d"], # nosied input audio # noqa: F722 + cond: float["b n d"], # masked cond audio # noqa: F722 + text: int["b nt"], # text # noqa: F722 + time: float["b"] | float[""], # time step # noqa: F821 F722 + drop_audio_cond, # cfg for cond audio + drop_text, # cfg for text + mask: bool["b n"] | None = None, # noqa: F722 + ): + batch, seq_len = x.shape[0], x.shape[1] + if time.ndim == 0: + time = time.repeat(batch) + + # t: conditioning time, c: context (text + masked cond audio), x: noised input audio + t = self.time_embed(time) + text_embed = self.text_embed(text, seq_len, drop_text=drop_text) + x = self.input_embed(x, cond, text_embed, drop_audio_cond=drop_audio_cond) + + rope = self.rotary_embed.forward_from_seq_len(seq_len) + + if self.long_skip_connection is not None: + residual = x + + for block in self.transformer_blocks: + x = block(x, t, mask=mask, rope=rope) + + if self.long_skip_connection is not None: + x = self.long_skip_connection(torch.cat((x, residual), dim=-1)) + + x = self.norm_out(x, t) + output = self.proj_out(x) + + return output diff --git a/src/f5_tts/model/backbones/mmdit.py b/src/f5_tts/model/backbones/mmdit.py new file mode 100644 index 0000000000000000000000000000000000000000..2da270ea8f50973e631a69df3baf707f0809717e --- /dev/null +++ b/src/f5_tts/model/backbones/mmdit.py @@ -0,0 +1,146 @@ +""" +ein notation: +b - batch +n - sequence +nt - text sequence +nw - raw wave length +d - dimension +""" + +from __future__ import annotations + +import torch +from torch import nn + +from x_transformers.x_transformers import RotaryEmbedding + +from f5_tts.model.modules import ( + TimestepEmbedding, + ConvPositionEmbedding, + MMDiTBlock, + AdaLayerNormZero_Final, + precompute_freqs_cis, + get_pos_embed_indices, +) + + +# text embedding + + +class TextEmbedding(nn.Module): + def __init__(self, out_dim, text_num_embeds): + super().__init__() + self.text_embed = nn.Embedding(text_num_embeds + 1, out_dim) # will use 0 as filler token + + self.precompute_max_pos = 1024 + self.register_buffer("freqs_cis", precompute_freqs_cis(out_dim, self.precompute_max_pos), persistent=False) + + def forward(self, text: int["b nt"], drop_text=False) -> int["b nt d"]: # noqa: F722 + text = text + 1 + if drop_text: + text = torch.zeros_like(text) + text = self.text_embed(text) + + # sinus pos emb + batch_start = torch.zeros((text.shape[0],), dtype=torch.long) + batch_text_len = text.shape[1] + pos_idx = get_pos_embed_indices(batch_start, batch_text_len, max_pos=self.precompute_max_pos) + text_pos_embed = self.freqs_cis[pos_idx] + + text = text + text_pos_embed + + return text + + +# noised input & masked cond audio embedding + + +class AudioEmbedding(nn.Module): + def __init__(self, in_dim, out_dim): + super().__init__() + self.linear = nn.Linear(2 * in_dim, out_dim) + self.conv_pos_embed = ConvPositionEmbedding(out_dim) + + def forward(self, x: float["b n d"], cond: float["b n d"], drop_audio_cond=False): # noqa: F722 + if drop_audio_cond: + cond = torch.zeros_like(cond) + x = torch.cat((x, cond), dim=-1) + x = self.linear(x) + x = self.conv_pos_embed(x) + x + return x + + +# Transformer backbone using MM-DiT blocks + + +class MMDiT(nn.Module): + def __init__( + self, + *, + dim, + depth=8, + heads=8, + dim_head=64, + dropout=0.1, + ff_mult=4, + text_num_embeds=256, + mel_dim=100, + ): + super().__init__() + + self.time_embed = TimestepEmbedding(dim) + self.text_embed = TextEmbedding(dim, text_num_embeds) + self.audio_embed = AudioEmbedding(mel_dim, dim) + + self.rotary_embed = RotaryEmbedding(dim_head) + + self.dim = dim + self.depth = depth + + self.transformer_blocks = nn.ModuleList( + [ + MMDiTBlock( + dim=dim, + heads=heads, + dim_head=dim_head, + dropout=dropout, + ff_mult=ff_mult, + context_pre_only=i == depth - 1, + ) + for i in range(depth) + ] + ) + self.norm_out = AdaLayerNormZero_Final(dim) # final modulation + self.proj_out = nn.Linear(dim, mel_dim) + + def forward( + self, + x: float["b n d"], # nosied input audio # noqa: F722 + cond: float["b n d"], # masked cond audio # noqa: F722 + text: int["b nt"], # text # noqa: F722 + time: float["b"] | float[""], # time step # noqa: F821 F722 + drop_audio_cond, # cfg for cond audio + drop_text, # cfg for text + mask: bool["b n"] | None = None, # noqa: F722 + ): + batch = x.shape[0] + if time.ndim == 0: + time = time.repeat(batch) + + # t: conditioning (time), c: context (text + masked cond audio), x: noised input audio + t = self.time_embed(time) + c = self.text_embed(text, drop_text=drop_text) + x = self.audio_embed(x, cond, drop_audio_cond=drop_audio_cond) + + seq_len = x.shape[1] + text_len = text.shape[1] + rope_audio = self.rotary_embed.forward_from_seq_len(seq_len) + rope_text = self.rotary_embed.forward_from_seq_len(text_len) + + for block in self.transformer_blocks: + c, x = block(x, c, t, mask=mask, rope=rope_audio, c_rope=rope_text) + + x = self.norm_out(x, t) + output = self.proj_out(x) + + return output diff --git a/src/f5_tts/model/backbones/unett.py b/src/f5_tts/model/backbones/unett.py new file mode 100644 index 0000000000000000000000000000000000000000..7980eead2b1ecba87c7006c45c93144280ff0d2d --- /dev/null +++ b/src/f5_tts/model/backbones/unett.py @@ -0,0 +1,219 @@ +""" +ein notation: +b - batch +n - sequence +nt - text sequence +nw - raw wave length +d - dimension +""" + +from __future__ import annotations +from typing import Literal + +import torch +from torch import nn +import torch.nn.functional as F + +from x_transformers import RMSNorm +from x_transformers.x_transformers import RotaryEmbedding + +from f5_tts.model.modules import ( + TimestepEmbedding, + ConvNeXtV2Block, + ConvPositionEmbedding, + Attention, + AttnProcessor, + FeedForward, + precompute_freqs_cis, + get_pos_embed_indices, +) + + +# Text embedding + + +class TextEmbedding(nn.Module): + def __init__(self, text_num_embeds, text_dim, conv_layers=0, conv_mult=2): + super().__init__() + self.text_embed = nn.Embedding(text_num_embeds + 1, text_dim) # use 0 as filler token + + if conv_layers > 0: + self.extra_modeling = True + self.precompute_max_pos = 4096 # ~44s of 24khz audio + self.register_buffer("freqs_cis", precompute_freqs_cis(text_dim, self.precompute_max_pos), persistent=False) + self.text_blocks = nn.Sequential( + *[ConvNeXtV2Block(text_dim, text_dim * conv_mult) for _ in range(conv_layers)] + ) + else: + self.extra_modeling = False + + def forward(self, text: int["b nt"], seq_len, drop_text=False): # noqa: F722 + text = text + 1 # use 0 as filler token. preprocess of batch pad -1, see list_str_to_idx() + text = text[:, :seq_len] # curtail if character tokens are more than the mel spec tokens + batch, text_len = text.shape[0], text.shape[1] + text = F.pad(text, (0, seq_len - text_len), value=0) + + if drop_text: # cfg for text + text = torch.zeros_like(text) + + text = self.text_embed(text) # b n -> b n d + + # possible extra modeling + if self.extra_modeling: + # sinus pos emb + batch_start = torch.zeros((batch,), dtype=torch.long) + pos_idx = get_pos_embed_indices(batch_start, seq_len, max_pos=self.precompute_max_pos) + text_pos_embed = self.freqs_cis[pos_idx] + text = text + text_pos_embed + + # convnextv2 blocks + text = self.text_blocks(text) + + return text + + +# noised input audio and context mixing embedding + + +class InputEmbedding(nn.Module): + def __init__(self, mel_dim, text_dim, out_dim): + super().__init__() + self.proj = nn.Linear(mel_dim * 2 + text_dim, out_dim) + self.conv_pos_embed = ConvPositionEmbedding(dim=out_dim) + + def forward(self, x: float["b n d"], cond: float["b n d"], text_embed: float["b n d"], drop_audio_cond=False): # noqa: F722 + if drop_audio_cond: # cfg for cond audio + cond = torch.zeros_like(cond) + + x = self.proj(torch.cat((x, cond, text_embed), dim=-1)) + x = self.conv_pos_embed(x) + x + return x + + +# Flat UNet Transformer backbone + + +class UNetT(nn.Module): + def __init__( + self, + *, + dim, + depth=8, + heads=8, + dim_head=64, + dropout=0.1, + ff_mult=4, + mel_dim=100, + text_num_embeds=256, + text_dim=None, + conv_layers=0, + skip_connect_type: Literal["add", "concat", "none"] = "concat", + ): + super().__init__() + assert depth % 2 == 0, "UNet-Transformer's depth should be even." + + self.time_embed = TimestepEmbedding(dim) + if text_dim is None: + text_dim = mel_dim + self.text_embed = TextEmbedding(text_num_embeds, text_dim, conv_layers=conv_layers) + self.input_embed = InputEmbedding(mel_dim, text_dim, dim) + + self.rotary_embed = RotaryEmbedding(dim_head) + + # transformer layers & skip connections + + self.dim = dim + self.skip_connect_type = skip_connect_type + needs_skip_proj = skip_connect_type == "concat" + + self.depth = depth + self.layers = nn.ModuleList([]) + + for idx in range(depth): + is_later_half = idx >= (depth // 2) + + attn_norm = RMSNorm(dim) + attn = Attention( + processor=AttnProcessor(), + dim=dim, + heads=heads, + dim_head=dim_head, + dropout=dropout, + ) + + ff_norm = RMSNorm(dim) + ff = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh") + + skip_proj = nn.Linear(dim * 2, dim, bias=False) if needs_skip_proj and is_later_half else None + + self.layers.append( + nn.ModuleList( + [ + skip_proj, + attn_norm, + attn, + ff_norm, + ff, + ] + ) + ) + + self.norm_out = RMSNorm(dim) + self.proj_out = nn.Linear(dim, mel_dim) + + def forward( + self, + x: float["b n d"], # nosied input audio # noqa: F722 + cond: float["b n d"], # masked cond audio # noqa: F722 + text: int["b nt"], # text # noqa: F722 + time: float["b"] | float[""], # time step # noqa: F821 F722 + drop_audio_cond, # cfg for cond audio + drop_text, # cfg for text + mask: bool["b n"] | None = None, # noqa: F722 + ): + batch, seq_len = x.shape[0], x.shape[1] + if time.ndim == 0: + time = time.repeat(batch) + + # t: conditioning time, c: context (text + masked cond audio), x: noised input audio + t = self.time_embed(time) + text_embed = self.text_embed(text, seq_len, drop_text=drop_text) + x = self.input_embed(x, cond, text_embed, drop_audio_cond=drop_audio_cond) + + # postfix time t to input x, [b n d] -> [b n+1 d] + x = torch.cat([t.unsqueeze(1), x], dim=1) # pack t to x + if mask is not None: + mask = F.pad(mask, (1, 0), value=1) + + rope = self.rotary_embed.forward_from_seq_len(seq_len + 1) + + # flat unet transformer + skip_connect_type = self.skip_connect_type + skips = [] + for idx, (maybe_skip_proj, attn_norm, attn, ff_norm, ff) in enumerate(self.layers): + layer = idx + 1 + + # skip connection logic + is_first_half = layer <= (self.depth // 2) + is_later_half = not is_first_half + + if is_first_half: + skips.append(x) + + if is_later_half: + skip = skips.pop() + if skip_connect_type == "concat": + x = torch.cat((x, skip), dim=-1) + x = maybe_skip_proj(x) + elif skip_connect_type == "add": + x = x + skip + + # attention and feedforward blocks + x = attn(attn_norm(x), rope=rope, mask=mask) + x + x = ff(ff_norm(x)) + x + + assert len(skips) == 0 + + x = self.norm_out(x)[:, 1:, :] # unpack t from x + + return self.proj_out(x) diff --git a/src/f5_tts/model/cfm.py b/src/f5_tts/model/cfm.py new file mode 100644 index 0000000000000000000000000000000000000000..abf6e45370911c2d347004c2dfa086220ec0ebb9 --- /dev/null +++ b/src/f5_tts/model/cfm.py @@ -0,0 +1,285 @@ +""" +ein notation: +b - batch +n - sequence +nt - text sequence +nw - raw wave length +d - dimension +""" + +from __future__ import annotations + +from random import random +from typing import Callable + +import torch +import torch.nn.functional as F +from torch import nn +from torch.nn.utils.rnn import pad_sequence +from torchdiffeq import odeint + +from f5_tts.model.modules import MelSpec +from f5_tts.model.utils import ( + default, + exists, + lens_to_mask, + list_str_to_idx, + list_str_to_tensor, + mask_from_frac_lengths, +) + + +class CFM(nn.Module): + def __init__( + self, + transformer: nn.Module, + sigma=0.0, + odeint_kwargs: dict = dict( + # atol = 1e-5, + # rtol = 1e-5, + method="euler" # 'midpoint' + ), + audio_drop_prob=0.3, + cond_drop_prob=0.2, + num_channels=None, + mel_spec_module: nn.Module | None = None, + mel_spec_kwargs: dict = dict(), + frac_lengths_mask: tuple[float, float] = (0.7, 1.0), + vocab_char_map: dict[str:int] | None = None, + ): + super().__init__() + + self.frac_lengths_mask = frac_lengths_mask + + # mel spec + self.mel_spec = default(mel_spec_module, MelSpec(**mel_spec_kwargs)) + num_channels = default(num_channels, self.mel_spec.n_mel_channels) + self.num_channels = num_channels + + # classifier-free guidance + self.audio_drop_prob = audio_drop_prob + self.cond_drop_prob = cond_drop_prob + + # transformer + self.transformer = transformer + dim = transformer.dim + self.dim = dim + + # conditional flow related + self.sigma = sigma + + # sampling related + self.odeint_kwargs = odeint_kwargs + + # vocab map for tokenization + self.vocab_char_map = vocab_char_map + + @property + def device(self): + return next(self.parameters()).device + + @torch.no_grad() + def sample( + self, + cond: float["b n d"] | float["b nw"], # noqa: F722 + text: int["b nt"] | list[str], # noqa: F722 + duration: int | int["b"], # noqa: F821 + *, + lens: int["b"] | None = None, # noqa: F821 + steps=32, + cfg_strength=1.0, + sway_sampling_coef=None, + seed: int | None = None, + max_duration=4096, + vocoder: Callable[[float["b d n"]], float["b nw"]] | None = None, # noqa: F722 + no_ref_audio=False, + duplicate_test=False, + t_inter=0.1, + edit_mask=None, + ): + self.eval() + # raw wave + + if cond.ndim == 2: + cond = self.mel_spec(cond) + cond = cond.permute(0, 2, 1) + assert cond.shape[-1] == self.num_channels + + cond = cond.to(next(self.parameters()).dtype) + + batch, cond_seq_len, device = *cond.shape[:2], cond.device + if not exists(lens): + lens = torch.full((batch,), cond_seq_len, device=device, dtype=torch.long) + + # text + + if isinstance(text, list): + if exists(self.vocab_char_map): + text = list_str_to_idx(text, self.vocab_char_map).to(device) + else: + text = list_str_to_tensor(text).to(device) + assert text.shape[0] == batch + + if exists(text): + text_lens = (text != -1).sum(dim=-1) + lens = torch.maximum(text_lens, lens) # make sure lengths are at least those of the text characters + + # duration + + cond_mask = lens_to_mask(lens) + if edit_mask is not None: + cond_mask = cond_mask & edit_mask + + if isinstance(duration, int): + duration = torch.full((batch,), duration, device=device, dtype=torch.long) + + duration = torch.maximum(lens + 1, duration) # just add one token so something is generated + duration = duration.clamp(max=max_duration) + max_duration = duration.amax() + + # duplicate test corner for inner time step oberservation + if duplicate_test: + test_cond = F.pad(cond, (0, 0, cond_seq_len, max_duration - 2 * cond_seq_len), value=0.0) + + cond = F.pad(cond, (0, 0, 0, max_duration - cond_seq_len), value=0.0) + cond_mask = F.pad(cond_mask, (0, max_duration - cond_mask.shape[-1]), value=False) + cond_mask = cond_mask.unsqueeze(-1) + step_cond = torch.where( + cond_mask, cond, torch.zeros_like(cond) + ) # allow direct control (cut cond audio) with lens passed in + + if batch > 1: + mask = lens_to_mask(duration) + else: # save memory and speed up, as single inference need no mask currently + mask = None + + # test for no ref audio + if no_ref_audio: + cond = torch.zeros_like(cond) + + # neural ode + + def fn(t, x): + # at each step, conditioning is fixed + # step_cond = torch.where(cond_mask, cond, torch.zeros_like(cond)) + + # predict flow + pred = self.transformer( + x=x, cond=step_cond, text=text, time=t, mask=mask, drop_audio_cond=False, drop_text=False + ) + if cfg_strength < 1e-5: + return pred + + null_pred = self.transformer( + x=x, cond=step_cond, text=text, time=t, mask=mask, drop_audio_cond=True, drop_text=True + ) + return pred + (pred - null_pred) * cfg_strength + + # noise input + # to make sure batch inference result is same with different batch size, and for sure single inference + # still some difference maybe due to convolutional layers + y0 = [] + for dur in duration: + if exists(seed): + torch.manual_seed(seed) + y0.append(torch.randn(dur, self.num_channels, device=self.device, dtype=step_cond.dtype)) + y0 = pad_sequence(y0, padding_value=0, batch_first=True) + + t_start = 0 + + # duplicate test corner for inner time step oberservation + if duplicate_test: + t_start = t_inter + y0 = (1 - t_start) * y0 + t_start * test_cond + steps = int(steps * (1 - t_start)) + + t = torch.linspace(t_start, 1, steps, device=self.device, dtype=step_cond.dtype) + if sway_sampling_coef is not None: + t = t + sway_sampling_coef * (torch.cos(torch.pi / 2 * t) - 1 + t) + + trajectory = odeint(fn, y0, t, **self.odeint_kwargs) + + sampled = trajectory[-1] + out = sampled + out = torch.where(cond_mask, cond, out) + + if exists(vocoder): + out = out.permute(0, 2, 1) + out = vocoder(out) + + return out, trajectory + + def forward( + self, + inp: float["b n d"] | float["b nw"], # mel or raw wave # noqa: F722 + text: int["b nt"] | list[str], # noqa: F722 + *, + lens: int["b"] | None = None, # noqa: F821 + noise_scheduler: str | None = None, + ): + # handle raw wave + if inp.ndim == 2: + inp = self.mel_spec(inp) + inp = inp.permute(0, 2, 1) + assert inp.shape[-1] == self.num_channels + + batch, seq_len, dtype, device, _σ1 = *inp.shape[:2], inp.dtype, self.device, self.sigma + + # handle text as string + if isinstance(text, list): + if exists(self.vocab_char_map): + text = list_str_to_idx(text, self.vocab_char_map).to(device) + else: + text = list_str_to_tensor(text).to(device) + assert text.shape[0] == batch + + # lens and mask + if not exists(lens): + lens = torch.full((batch,), seq_len, device=device) + + mask = lens_to_mask(lens, length=seq_len) # useless here, as collate_fn will pad to max length in batch + + # get a random span to mask out for training conditionally + frac_lengths = torch.zeros((batch,), device=self.device).float().uniform_(*self.frac_lengths_mask) + rand_span_mask = mask_from_frac_lengths(lens, frac_lengths) + + if exists(mask): + rand_span_mask &= mask + + # mel is x1 + x1 = inp + + # x0 is gaussian noise + x0 = torch.randn_like(x1) + + # time step + time = torch.rand((batch,), dtype=dtype, device=self.device) + # TODO. noise_scheduler + + # sample xt (φ_t(x) in the paper) + t = time.unsqueeze(-1).unsqueeze(-1) + φ = (1 - t) * x0 + t * x1 + flow = x1 - x0 + + # only predict what is within the random mask span for infilling + cond = torch.where(rand_span_mask[..., None], torch.zeros_like(x1), x1) + + # transformer and cfg training with a drop rate + drop_audio_cond = random() < self.audio_drop_prob # p_drop in voicebox paper + if random() < self.cond_drop_prob: # p_uncond in voicebox paper + drop_audio_cond = True + drop_text = True + else: + drop_text = False + + # if want rigourously mask out padding, record in collate_fn in dataset.py, and pass in here + # adding mask will use more memory, thus also need to adjust batchsampler with scaled down threshold for long sequences + pred = self.transformer( + x=φ, cond=cond, text=text, time=time, drop_audio_cond=drop_audio_cond, drop_text=drop_text + ) + + # flow matching loss + loss = F.mse_loss(pred, flow, reduction="none") + loss = loss[rand_span_mask] + + return loss.mean(), cond, pred diff --git a/src/f5_tts/model/dataset.py b/src/f5_tts/model/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..a99bcb522c383dfa75a2592c6c010490b184ddeb --- /dev/null +++ b/src/f5_tts/model/dataset.py @@ -0,0 +1,314 @@ +import json +import random +from importlib.resources import files + +import torch +import torch.nn.functional as F +import torchaudio +from datasets import Dataset as Dataset_ +from datasets import load_from_disk +from torch import nn +from torch.utils.data import Dataset, Sampler +from tqdm import tqdm + +from f5_tts.model.modules import MelSpec +from f5_tts.model.utils import default + + +class HFDataset(Dataset): + def __init__( + self, + hf_dataset: Dataset, + target_sample_rate=24_000, + n_mel_channels=100, + hop_length=256, + n_fft=1024, + win_length=1024, + mel_spec_type="vocos", + ): + self.data = hf_dataset + self.target_sample_rate = target_sample_rate + self.hop_length = hop_length + + self.mel_spectrogram = MelSpec( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ) + + def get_frame_len(self, index): + row = self.data[index] + audio = row["audio"]["array"] + sample_rate = row["audio"]["sampling_rate"] + return audio.shape[-1] / sample_rate * self.target_sample_rate / self.hop_length + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + row = self.data[index] + audio = row["audio"]["array"] + + # logger.info(f"Audio shape: {audio.shape}") + + sample_rate = row["audio"]["sampling_rate"] + duration = audio.shape[-1] / sample_rate + + if duration > 30 or duration < 0.3: + return self.__getitem__((index + 1) % len(self.data)) + + audio_tensor = torch.from_numpy(audio).float() + + if sample_rate != self.target_sample_rate: + resampler = torchaudio.transforms.Resample(sample_rate, self.target_sample_rate) + audio_tensor = resampler(audio_tensor) + + audio_tensor = audio_tensor.unsqueeze(0) # 't -> 1 t') + + mel_spec = self.mel_spectrogram(audio_tensor) + + mel_spec = mel_spec.squeeze(0) # '1 d t -> d t' + + text = row["text"] + + return dict( + mel_spec=mel_spec, + text=text, + ) + + +class CustomDataset(Dataset): + def __init__( + self, + custom_dataset: Dataset, + durations=None, + target_sample_rate=24_000, + hop_length=256, + n_mel_channels=100, + n_fft=1024, + win_length=1024, + mel_spec_type="vocos", + preprocessed_mel=False, + mel_spec_module: nn.Module | None = None, + ): + self.data = custom_dataset + self.durations = durations + self.target_sample_rate = target_sample_rate + self.hop_length = hop_length + self.n_fft = n_fft + self.win_length = win_length + self.mel_spec_type = mel_spec_type + self.preprocessed_mel = preprocessed_mel + + if not preprocessed_mel: + self.mel_spectrogram = default( + mel_spec_module, + MelSpec( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ), + ) + + def get_frame_len(self, index): + if ( + self.durations is not None + ): # Please make sure the separately provided durations are correct, otherwise 99.99% OOM + return self.durations[index] * self.target_sample_rate / self.hop_length + return self.data[index]["duration"] * self.target_sample_rate / self.hop_length + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + row = self.data[index] + audio_path = row["audio_path"] + text = row["text"] + duration = row["duration"] + + if self.preprocessed_mel: + mel_spec = torch.tensor(row["mel_spec"]) + + else: + audio, source_sample_rate = torchaudio.load(audio_path) + if audio.shape[0] > 1: + audio = torch.mean(audio, dim=0, keepdim=True) + + if duration > 30 or duration < 0.3: + return self.__getitem__((index + 1) % len(self.data)) + + if source_sample_rate != self.target_sample_rate: + resampler = torchaudio.transforms.Resample(source_sample_rate, self.target_sample_rate) + audio = resampler(audio) + + mel_spec = self.mel_spectrogram(audio) + mel_spec = mel_spec.squeeze(0) # '1 d t -> d t') + + return dict( + mel_spec=mel_spec, + text=text, + ) + + +# Dynamic Batch Sampler + + +class DynamicBatchSampler(Sampler[list[int]]): + """Extension of Sampler that will do the following: + 1. Change the batch size (essentially number of sequences) + in a batch to ensure that the total number of frames are less + than a certain threshold. + 2. Make sure the padding efficiency in the batch is high. + """ + + def __init__( + self, sampler: Sampler[int], frames_threshold: int, max_samples=0, random_seed=None, drop_last: bool = False + ): + self.sampler = sampler + self.frames_threshold = frames_threshold + self.max_samples = max_samples + + indices, batches = [], [] + data_source = self.sampler.data_source + + for idx in tqdm( + self.sampler, desc="Sorting with sampler... if slow, check whether dataset is provided with duration" + ): + indices.append((idx, data_source.get_frame_len(idx))) + indices.sort(key=lambda elem: elem[1]) + + batch = [] + batch_frames = 0 + for idx, frame_len in tqdm( + indices, desc=f"Creating dynamic batches with {frames_threshold} audio frames per gpu" + ): + if batch_frames + frame_len <= self.frames_threshold and (max_samples == 0 or len(batch) < max_samples): + batch.append(idx) + batch_frames += frame_len + else: + if len(batch) > 0: + batches.append(batch) + if frame_len <= self.frames_threshold: + batch = [idx] + batch_frames = frame_len + else: + batch = [] + batch_frames = 0 + + if not drop_last and len(batch) > 0: + batches.append(batch) + + del indices + + # if want to have different batches between epochs, may just set a seed and log it in ckpt + # cuz during multi-gpu training, although the batch on per gpu not change between epochs, the formed general minibatch is different + # e.g. for epoch n, use (random_seed + n) + random.seed(random_seed) + random.shuffle(batches) + + self.batches = batches + + def __iter__(self): + return iter(self.batches) + + def __len__(self): + return len(self.batches) + + +# Load dataset + + +def load_dataset( + dataset_name: str, + tokenizer: str = "pinyin", + dataset_type: str = "CustomDataset", + audio_type: str = "raw", + mel_spec_module: nn.Module | None = None, + mel_spec_kwargs: dict = dict(), +) -> CustomDataset | HFDataset: + """ + dataset_type - "CustomDataset" if you want to use tokenizer name and default data path to load for train_dataset + - "CustomDatasetPath" if you just want to pass the full path to a preprocessed dataset without relying on tokenizer + """ + + print("Loading dataset ...") + + if dataset_type == "CustomDataset": + rel_data_path = str(files("f5_tts").joinpath(f"../../data/{dataset_name}_{tokenizer}")) + if audio_type == "raw": + try: + train_dataset = load_from_disk(f"{rel_data_path}/raw") + except: # noqa: E722 + train_dataset = Dataset_.from_file(f"{rel_data_path}/raw.arrow") + preprocessed_mel = False + elif audio_type == "mel": + train_dataset = Dataset_.from_file(f"{rel_data_path}/mel.arrow") + preprocessed_mel = True + with open(f"{rel_data_path}/duration.json", "r", encoding="utf-8") as f: + data_dict = json.load(f) + durations = data_dict["duration"] + train_dataset = CustomDataset( + train_dataset, + durations=durations, + preprocessed_mel=preprocessed_mel, + mel_spec_module=mel_spec_module, + **mel_spec_kwargs, + ) + + elif dataset_type == "CustomDatasetPath": + try: + train_dataset = load_from_disk(f"{dataset_name}/raw") + except: # noqa: E722 + train_dataset = Dataset_.from_file(f"{dataset_name}/raw.arrow") + + with open(f"{dataset_name}/duration.json", "r", encoding="utf-8") as f: + data_dict = json.load(f) + durations = data_dict["duration"] + train_dataset = CustomDataset( + train_dataset, durations=durations, preprocessed_mel=preprocessed_mel, **mel_spec_kwargs + ) + + elif dataset_type == "HFDataset": + print( + "Should manually modify the path of huggingface dataset to your need.\n" + + "May also the corresponding script cuz different dataset may have different format." + ) + pre, post = dataset_name.split("_") + train_dataset = HFDataset( + load_dataset(f"{pre}/{pre}", split=f"train.{post}", cache_dir=str(files("f5_tts").joinpath("../../data"))), + ) + + return train_dataset + + +# collation + + +def collate_fn(batch): + mel_specs = [item["mel_spec"].squeeze(0) for item in batch] + mel_lengths = torch.LongTensor([spec.shape[-1] for spec in mel_specs]) + max_mel_length = mel_lengths.amax() + + padded_mel_specs = [] + for spec in mel_specs: # TODO. maybe records mask for attention here + padding = (0, max_mel_length - spec.size(-1)) + padded_spec = F.pad(spec, padding, value=0) + padded_mel_specs.append(padded_spec) + + mel_specs = torch.stack(padded_mel_specs) + + text = [item["text"] for item in batch] + text_lengths = torch.LongTensor([len(item) for item in text]) + + return dict( + mel=mel_specs, + mel_lengths=mel_lengths, + text=text, + text_lengths=text_lengths, + ) diff --git a/src/f5_tts/model/modules.py b/src/f5_tts/model/modules.py new file mode 100644 index 0000000000000000000000000000000000000000..4844f467a2e4067cb2202efe3d89cb6d640fa132 --- /dev/null +++ b/src/f5_tts/model/modules.py @@ -0,0 +1,658 @@ +""" +ein notation: +b - batch +n - sequence +nt - text sequence +nw - raw wave length +d - dimension +""" + +from __future__ import annotations + +import math +from typing import Optional + +import torch +import torch.nn.functional as F +import torchaudio +from librosa.filters import mel as librosa_mel_fn +from torch import nn +from x_transformers.x_transformers import apply_rotary_pos_emb + + +# raw wav to mel spec + + +mel_basis_cache = {} +hann_window_cache = {} + + +def get_bigvgan_mel_spectrogram( + waveform, + n_fft=1024, + n_mel_channels=100, + target_sample_rate=24000, + hop_length=256, + win_length=1024, + fmin=0, + fmax=None, + center=False, +): # Copy from https://github.com/NVIDIA/BigVGAN/tree/main + device = waveform.device + key = f"{n_fft}_{n_mel_channels}_{target_sample_rate}_{hop_length}_{win_length}_{fmin}_{fmax}_{device}" + + if key not in mel_basis_cache: + mel = librosa_mel_fn(sr=target_sample_rate, n_fft=n_fft, n_mels=n_mel_channels, fmin=fmin, fmax=fmax) + mel_basis_cache[key] = torch.from_numpy(mel).float().to(device) # TODO: why they need .float()? + hann_window_cache[key] = torch.hann_window(win_length).to(device) + + mel_basis = mel_basis_cache[key] + hann_window = hann_window_cache[key] + + padding = (n_fft - hop_length) // 2 + waveform = torch.nn.functional.pad(waveform.unsqueeze(1), (padding, padding), mode="reflect").squeeze(1) + + spec = torch.stft( + waveform, + n_fft, + hop_length=hop_length, + win_length=win_length, + window=hann_window, + center=center, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + spec = torch.sqrt(torch.view_as_real(spec).pow(2).sum(-1) + 1e-9) + + mel_spec = torch.matmul(mel_basis, spec) + mel_spec = torch.log(torch.clamp(mel_spec, min=1e-5)) + + return mel_spec + + +def get_vocos_mel_spectrogram( + waveform, + n_fft=1024, + n_mel_channels=100, + target_sample_rate=24000, + hop_length=256, + win_length=1024, +): + mel_stft = torchaudio.transforms.MelSpectrogram( + sample_rate=target_sample_rate, + n_fft=n_fft, + win_length=win_length, + hop_length=hop_length, + n_mels=n_mel_channels, + power=1, + center=True, + normalized=False, + norm=None, + ).to(waveform.device) + if len(waveform.shape) == 3: + waveform = waveform.squeeze(1) # 'b 1 nw -> b nw' + + assert len(waveform.shape) == 2 + + mel = mel_stft(waveform) + mel = mel.clamp(min=1e-5).log() + return mel + + +class MelSpec(nn.Module): + def __init__( + self, + n_fft=1024, + hop_length=256, + win_length=1024, + n_mel_channels=100, + target_sample_rate=24_000, + mel_spec_type="vocos", + ): + super().__init__() + assert mel_spec_type in ["vocos", "bigvgan"], print("We only support two extract mel backend: vocos or bigvgan") + + self.n_fft = n_fft + self.hop_length = hop_length + self.win_length = win_length + self.n_mel_channels = n_mel_channels + self.target_sample_rate = target_sample_rate + + if mel_spec_type == "vocos": + self.extractor = get_vocos_mel_spectrogram + elif mel_spec_type == "bigvgan": + self.extractor = get_bigvgan_mel_spectrogram + + self.register_buffer("dummy", torch.tensor(0), persistent=False) + + def forward(self, wav): + if self.dummy.device != wav.device: + self.to(wav.device) + + mel = self.extractor( + waveform=wav, + n_fft=self.n_fft, + n_mel_channels=self.n_mel_channels, + target_sample_rate=self.target_sample_rate, + hop_length=self.hop_length, + win_length=self.win_length, + ) + + return mel + + +# sinusoidal position embedding + + +class SinusPositionEmbedding(nn.Module): + def __init__(self, dim): + super().__init__() + self.dim = dim + + def forward(self, x, scale=1000): + device = x.device + half_dim = self.dim // 2 + emb = math.log(10000) / (half_dim - 1) + emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb) + emb = scale * x.unsqueeze(1) * emb.unsqueeze(0) + emb = torch.cat((emb.sin(), emb.cos()), dim=-1) + return emb + + +# convolutional position embedding + + +class ConvPositionEmbedding(nn.Module): + def __init__(self, dim, kernel_size=31, groups=16): + super().__init__() + assert kernel_size % 2 != 0 + self.conv1d = nn.Sequential( + nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2), + nn.Mish(), + nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2), + nn.Mish(), + ) + + def forward(self, x: float["b n d"], mask: bool["b n"] | None = None): # noqa: F722 + if mask is not None: + mask = mask[..., None] + x = x.masked_fill(~mask, 0.0) + + x = x.permute(0, 2, 1) + x = self.conv1d(x) + out = x.permute(0, 2, 1) + + if mask is not None: + out = out.masked_fill(~mask, 0.0) + + return out + + +# rotary positional embedding related + + +def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, theta_rescale_factor=1.0): + # proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning + # has some connection to NTK literature + # https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/ + # https://github.com/lucidrains/rotary-embedding-torch/blob/main/rotary_embedding_torch/rotary_embedding_torch.py + theta *= theta_rescale_factor ** (dim / (dim - 2)) + freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) + t = torch.arange(end, device=freqs.device) # type: ignore + freqs = torch.outer(t, freqs).float() # type: ignore + freqs_cos = torch.cos(freqs) # real part + freqs_sin = torch.sin(freqs) # imaginary part + return torch.cat([freqs_cos, freqs_sin], dim=-1) + + +def get_pos_embed_indices(start, length, max_pos, scale=1.0): + # length = length if isinstance(length, int) else length.max() + scale = scale * torch.ones_like(start, dtype=torch.float32) # in case scale is a scalar + pos = ( + start.unsqueeze(1) + + (torch.arange(length, device=start.device, dtype=torch.float32).unsqueeze(0) * scale.unsqueeze(1)).long() + ) + # avoid extra long error. + pos = torch.where(pos < max_pos, pos, max_pos - 1) + return pos + + +# Global Response Normalization layer (Instance Normalization ?) + + +class GRN(nn.Module): + def __init__(self, dim): + super().__init__() + self.gamma = nn.Parameter(torch.zeros(1, 1, dim)) + self.beta = nn.Parameter(torch.zeros(1, 1, dim)) + + def forward(self, x): + Gx = torch.norm(x, p=2, dim=1, keepdim=True) + Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6) + return self.gamma * (x * Nx) + self.beta + x + + +# ConvNeXt-V2 Block https://github.com/facebookresearch/ConvNeXt-V2/blob/main/models/convnextv2.py +# ref: https://github.com/bfs18/e2_tts/blob/main/rfwave/modules.py#L108 + + +class ConvNeXtV2Block(nn.Module): + def __init__( + self, + dim: int, + intermediate_dim: int, + dilation: int = 1, + ): + super().__init__() + padding = (dilation * (7 - 1)) // 2 + self.dwconv = nn.Conv1d( + dim, dim, kernel_size=7, padding=padding, groups=dim, dilation=dilation + ) # depthwise conv + self.norm = nn.LayerNorm(dim, eps=1e-6) + self.pwconv1 = nn.Linear(dim, intermediate_dim) # pointwise/1x1 convs, implemented with linear layers + self.act = nn.GELU() + self.grn = GRN(intermediate_dim) + self.pwconv2 = nn.Linear(intermediate_dim, dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x = x.transpose(1, 2) # b n d -> b d n + x = self.dwconv(x) + x = x.transpose(1, 2) # b d n -> b n d + x = self.norm(x) + x = self.pwconv1(x) + x = self.act(x) + x = self.grn(x) + x = self.pwconv2(x) + return residual + x + + +# AdaLayerNormZero +# return with modulated x for attn input, and params for later mlp modulation + + +class AdaLayerNormZero(nn.Module): + def __init__(self, dim): + super().__init__() + + self.silu = nn.SiLU() + self.linear = nn.Linear(dim, dim * 6) + + self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + + def forward(self, x, emb=None): + emb = self.linear(self.silu(emb)) + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = torch.chunk(emb, 6, dim=1) + + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x, gate_msa, shift_mlp, scale_mlp, gate_mlp + + +# AdaLayerNormZero for final layer +# return only with modulated x for attn input, cuz no more mlp modulation + + +class AdaLayerNormZero_Final(nn.Module): + def __init__(self, dim): + super().__init__() + + self.silu = nn.SiLU() + self.linear = nn.Linear(dim, dim * 2) + + self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + + def forward(self, x, emb): + emb = self.linear(self.silu(emb)) + scale, shift = torch.chunk(emb, 2, dim=1) + + x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :] + return x + + +# FeedForward + + +class FeedForward(nn.Module): + def __init__(self, dim, dim_out=None, mult=4, dropout=0.0, approximate: str = "none"): + super().__init__() + inner_dim = int(dim * mult) + dim_out = dim_out if dim_out is not None else dim + + activation = nn.GELU(approximate=approximate) + project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation) + self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out)) + + def forward(self, x): + return self.ff(x) + + +# Attention with possible joint part +# modified from diffusers/src/diffusers/models/attention_processor.py + + +class Attention(nn.Module): + def __init__( + self, + processor: JointAttnProcessor | AttnProcessor, + dim: int, + heads: int = 8, + dim_head: int = 64, + dropout: float = 0.0, + context_dim: Optional[int] = None, # if not None -> joint attention + context_pre_only=None, + ): + super().__init__() + + if not hasattr(F, "scaled_dot_product_attention"): + raise ImportError("Attention equires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") + + self.processor = processor + + self.dim = dim + self.heads = heads + self.inner_dim = dim_head * heads + self.dropout = dropout + + self.context_dim = context_dim + self.context_pre_only = context_pre_only + + self.to_q = nn.Linear(dim, self.inner_dim) + self.to_k = nn.Linear(dim, self.inner_dim) + self.to_v = nn.Linear(dim, self.inner_dim) + + if self.context_dim is not None: + self.to_k_c = nn.Linear(context_dim, self.inner_dim) + self.to_v_c = nn.Linear(context_dim, self.inner_dim) + if self.context_pre_only is not None: + self.to_q_c = nn.Linear(context_dim, self.inner_dim) + + self.to_out = nn.ModuleList([]) + self.to_out.append(nn.Linear(self.inner_dim, dim)) + self.to_out.append(nn.Dropout(dropout)) + + if self.context_pre_only is not None and not self.context_pre_only: + self.to_out_c = nn.Linear(self.inner_dim, dim) + + def forward( + self, + x: float["b n d"], # noised input x # noqa: F722 + c: float["b n d"] = None, # context c # noqa: F722 + mask: bool["b n"] | None = None, # noqa: F722 + rope=None, # rotary position embedding for x + c_rope=None, # rotary position embedding for c + ) -> torch.Tensor: + if c is not None: + return self.processor(self, x, c=c, mask=mask, rope=rope, c_rope=c_rope) + else: + return self.processor(self, x, mask=mask, rope=rope) + + +# Attention processor + + +class AttnProcessor: + def __init__(self): + pass + + def __call__( + self, + attn: Attention, + x: float["b n d"], # noised input x # noqa: F722 + mask: bool["b n"] | None = None, # noqa: F722 + rope=None, # rotary position embedding + ) -> torch.FloatTensor: + batch_size = x.shape[0] + + # `sample` projections. + query = attn.to_q(x) + key = attn.to_k(x) + value = attn.to_v(x) + + # apply rotary position embedding + if rope is not None: + freqs, xpos_scale = rope + q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0) + + query = apply_rotary_pos_emb(query, freqs, q_xpos_scale) + key = apply_rotary_pos_emb(key, freqs, k_xpos_scale) + + # attention + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # mask. e.g. inference got a batch with different target durations, mask out the padding + if mask is not None: + attn_mask = mask + attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n' + attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2]) + else: + attn_mask = None + + x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False) + x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + x = x.to(query.dtype) + + # linear proj + x = attn.to_out[0](x) + # dropout + x = attn.to_out[1](x) + + if mask is not None: + mask = mask.unsqueeze(-1) + x = x.masked_fill(~mask, 0.0) + + return x + + +# Joint Attention processor for MM-DiT +# modified from diffusers/src/diffusers/models/attention_processor.py + + +class JointAttnProcessor: + def __init__(self): + pass + + def __call__( + self, + attn: Attention, + x: float["b n d"], # noised input x # noqa: F722 + c: float["b nt d"] = None, # context c, here text # noqa: F722 + mask: bool["b n"] | None = None, # noqa: F722 + rope=None, # rotary position embedding for x + c_rope=None, # rotary position embedding for c + ) -> torch.FloatTensor: + residual = x + + batch_size = c.shape[0] + + # `sample` projections. + query = attn.to_q(x) + key = attn.to_k(x) + value = attn.to_v(x) + + # `context` projections. + c_query = attn.to_q_c(c) + c_key = attn.to_k_c(c) + c_value = attn.to_v_c(c) + + # apply rope for context and noised input independently + if rope is not None: + freqs, xpos_scale = rope + q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0) + query = apply_rotary_pos_emb(query, freqs, q_xpos_scale) + key = apply_rotary_pos_emb(key, freqs, k_xpos_scale) + if c_rope is not None: + freqs, xpos_scale = c_rope + q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0) + c_query = apply_rotary_pos_emb(c_query, freqs, q_xpos_scale) + c_key = apply_rotary_pos_emb(c_key, freqs, k_xpos_scale) + + # attention + query = torch.cat([query, c_query], dim=1) + key = torch.cat([key, c_key], dim=1) + value = torch.cat([value, c_value], dim=1) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # mask. e.g. inference got a batch with different target durations, mask out the padding + if mask is not None: + attn_mask = F.pad(mask, (0, c.shape[1]), value=True) # no mask for c (text) + attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n' + attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2]) + else: + attn_mask = None + + x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False) + x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + x = x.to(query.dtype) + + # Split the attention outputs. + x, c = ( + x[:, : residual.shape[1]], + x[:, residual.shape[1] :], + ) + + # linear proj + x = attn.to_out[0](x) + # dropout + x = attn.to_out[1](x) + if not attn.context_pre_only: + c = attn.to_out_c(c) + + if mask is not None: + mask = mask.unsqueeze(-1) + x = x.masked_fill(~mask, 0.0) + # c = c.masked_fill(~mask, 0.) # no mask for c (text) + + return x, c + + +# DiT Block + + +class DiTBlock(nn.Module): + def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1): + super().__init__() + + self.attn_norm = AdaLayerNormZero(dim) + self.attn = Attention( + processor=AttnProcessor(), + dim=dim, + heads=heads, + dim_head=dim_head, + dropout=dropout, + ) + + self.ff_norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + self.ff = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh") + + def forward(self, x, t, mask=None, rope=None): # x: noised input, t: time embedding + # pre-norm & modulation for attention input + norm, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.attn_norm(x, emb=t) + + # attention + attn_output = self.attn(x=norm, mask=mask, rope=rope) + + # process attention output for input x + x = x + gate_msa.unsqueeze(1) * attn_output + + norm = self.ff_norm(x) * (1 + scale_mlp[:, None]) + shift_mlp[:, None] + ff_output = self.ff(norm) + x = x + gate_mlp.unsqueeze(1) * ff_output + + return x + + +# MMDiT Block https://arxiv.org/abs/2403.03206 + + +class MMDiTBlock(nn.Module): + r""" + modified from diffusers/src/diffusers/models/attention.py + + notes. + _c: context related. text, cond, etc. (left part in sd3 fig2.b) + _x: noised input related. (right part) + context_pre_only: last layer only do prenorm + modulation cuz no more ffn + """ + + def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1, context_pre_only=False): + super().__init__() + + self.context_pre_only = context_pre_only + + self.attn_norm_c = AdaLayerNormZero_Final(dim) if context_pre_only else AdaLayerNormZero(dim) + self.attn_norm_x = AdaLayerNormZero(dim) + self.attn = Attention( + processor=JointAttnProcessor(), + dim=dim, + heads=heads, + dim_head=dim_head, + dropout=dropout, + context_dim=dim, + context_pre_only=context_pre_only, + ) + + if not context_pre_only: + self.ff_norm_c = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + self.ff_c = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh") + else: + self.ff_norm_c = None + self.ff_c = None + self.ff_norm_x = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + self.ff_x = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh") + + def forward(self, x, c, t, mask=None, rope=None, c_rope=None): # x: noised input, c: context, t: time embedding + # pre-norm & modulation for attention input + if self.context_pre_only: + norm_c = self.attn_norm_c(c, t) + else: + norm_c, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.attn_norm_c(c, emb=t) + norm_x, x_gate_msa, x_shift_mlp, x_scale_mlp, x_gate_mlp = self.attn_norm_x(x, emb=t) + + # attention + x_attn_output, c_attn_output = self.attn(x=norm_x, c=norm_c, mask=mask, rope=rope, c_rope=c_rope) + + # process attention output for context c + if self.context_pre_only: + c = None + else: # if not last layer + c = c + c_gate_msa.unsqueeze(1) * c_attn_output + + norm_c = self.ff_norm_c(c) * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None] + c_ff_output = self.ff_c(norm_c) + c = c + c_gate_mlp.unsqueeze(1) * c_ff_output + + # process attention output for input x + x = x + x_gate_msa.unsqueeze(1) * x_attn_output + + norm_x = self.ff_norm_x(x) * (1 + x_scale_mlp[:, None]) + x_shift_mlp[:, None] + x_ff_output = self.ff_x(norm_x) + x = x + x_gate_mlp.unsqueeze(1) * x_ff_output + + return c, x + + +# time step conditioning embedding + + +class TimestepEmbedding(nn.Module): + def __init__(self, dim, freq_embed_dim=256): + super().__init__() + self.time_embed = SinusPositionEmbedding(freq_embed_dim) + self.time_mlp = nn.Sequential(nn.Linear(freq_embed_dim, dim), nn.SiLU(), nn.Linear(dim, dim)) + + def forward(self, timestep: float["b"]): # noqa: F821 + time_hidden = self.time_embed(timestep) + time_hidden = time_hidden.to(timestep.dtype) + time = self.time_mlp(time_hidden) # b d + return time diff --git a/src/f5_tts/model/trainer.py b/src/f5_tts/model/trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..13efb810903de04757f46cf47f0f83b4b32da9c2 --- /dev/null +++ b/src/f5_tts/model/trainer.py @@ -0,0 +1,353 @@ +from __future__ import annotations + +import gc +import os + +import torch +import torchaudio +import wandb +from accelerate import Accelerator +from accelerate.utils import DistributedDataParallelKwargs +from ema_pytorch import EMA +from torch.optim import AdamW +from torch.optim.lr_scheduler import LinearLR, SequentialLR +from torch.utils.data import DataLoader, Dataset, SequentialSampler +from tqdm import tqdm + +from f5_tts.model import CFM +from f5_tts.model.dataset import DynamicBatchSampler, collate_fn +from f5_tts.model.utils import default, exists + +# trainer + + +class Trainer: + def __init__( + self, + model: CFM, + epochs, + learning_rate, + num_warmup_updates=20000, + save_per_updates=1000, + checkpoint_path=None, + batch_size=32, + batch_size_type: str = "sample", + max_samples=32, + grad_accumulation_steps=1, + max_grad_norm=1.0, + noise_scheduler: str | None = None, + duration_predictor: torch.nn.Module | None = None, + logger: str | None = "wandb", # "wandb" | "tensorboard" | None + wandb_project="test_e2-tts", + wandb_run_name="test_run", + wandb_resume_id: str = None, + log_samples: bool = False, + last_per_steps=None, + accelerate_kwargs: dict = dict(), + ema_kwargs: dict = dict(), + bnb_optimizer: bool = False, + mel_spec_type: str = "vocos", # "vocos" | "bigvgan" + ): + ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) + + if logger == "wandb" and not wandb.api.api_key: + logger = None + print(f"Using logger: {logger}") + self.log_samples = log_samples + + self.accelerator = Accelerator( + log_with=logger if logger == "wandb" else None, + kwargs_handlers=[ddp_kwargs], + gradient_accumulation_steps=grad_accumulation_steps, + **accelerate_kwargs, + ) + + self.logger = logger + if self.logger == "wandb": + if exists(wandb_resume_id): + init_kwargs = {"wandb": {"resume": "allow", "name": wandb_run_name, "id": wandb_resume_id}} + else: + init_kwargs = {"wandb": {"resume": "allow", "name": wandb_run_name}} + + self.accelerator.init_trackers( + project_name=wandb_project, + init_kwargs=init_kwargs, + config={ + "epochs": epochs, + "learning_rate": learning_rate, + "num_warmup_updates": num_warmup_updates, + "batch_size": batch_size, + "batch_size_type": batch_size_type, + "max_samples": max_samples, + "grad_accumulation_steps": grad_accumulation_steps, + "max_grad_norm": max_grad_norm, + "gpus": self.accelerator.num_processes, + "noise_scheduler": noise_scheduler, + }, + ) + + elif self.logger == "tensorboard": + from torch.utils.tensorboard import SummaryWriter + + self.writer = SummaryWriter(log_dir=f"runs/{wandb_run_name}") + + self.model = model + + if self.is_main: + self.ema_model = EMA(model, include_online_model=False, **ema_kwargs) + self.ema_model.to(self.accelerator.device) + + self.epochs = epochs + self.num_warmup_updates = num_warmup_updates + self.save_per_updates = save_per_updates + self.last_per_steps = default(last_per_steps, save_per_updates * grad_accumulation_steps) + self.checkpoint_path = default(checkpoint_path, "ckpts/test_e2-tts") + + self.batch_size = batch_size + self.batch_size_type = batch_size_type + self.max_samples = max_samples + self.grad_accumulation_steps = grad_accumulation_steps + self.max_grad_norm = max_grad_norm + self.vocoder_name = mel_spec_type + + self.noise_scheduler = noise_scheduler + + self.duration_predictor = duration_predictor + + if bnb_optimizer: + import bitsandbytes as bnb + + self.optimizer = bnb.optim.AdamW8bit(model.parameters(), lr=learning_rate) + else: + self.optimizer = AdamW(model.parameters(), lr=learning_rate) + self.model, self.optimizer = self.accelerator.prepare(self.model, self.optimizer) + + @property + def is_main(self): + return self.accelerator.is_main_process + + def save_checkpoint(self, step, last=False): + self.accelerator.wait_for_everyone() + if self.is_main: + checkpoint = dict( + model_state_dict=self.accelerator.unwrap_model(self.model).state_dict(), + optimizer_state_dict=self.accelerator.unwrap_model(self.optimizer).state_dict(), + ema_model_state_dict=self.ema_model.state_dict(), + scheduler_state_dict=self.scheduler.state_dict(), + step=step, + ) + if not os.path.exists(self.checkpoint_path): + os.makedirs(self.checkpoint_path) + if last: + self.accelerator.save(checkpoint, f"{self.checkpoint_path}/model_last.pt") + print(f"Saved last checkpoint at step {step}") + else: + self.accelerator.save(checkpoint, f"{self.checkpoint_path}/model_{step}.pt") + + def load_checkpoint(self): + if ( + not exists(self.checkpoint_path) + or not os.path.exists(self.checkpoint_path) + or not os.listdir(self.checkpoint_path) + ): + return 0 + + self.accelerator.wait_for_everyone() + if "model_last.pt" in os.listdir(self.checkpoint_path): + latest_checkpoint = "model_last.pt" + else: + latest_checkpoint = sorted( + [f for f in os.listdir(self.checkpoint_path) if f.endswith(".pt")], + key=lambda x: int("".join(filter(str.isdigit, x))), + )[-1] + # checkpoint = torch.load(f"{self.checkpoint_path}/{latest_checkpoint}", map_location=self.accelerator.device) # rather use accelerator.load_state ಥ_ಥ + checkpoint = torch.load(f"{self.checkpoint_path}/{latest_checkpoint}", weights_only=True, map_location="cpu") + + # patch for backward compatibility, 305e3ea + for key in ["ema_model.mel_spec.mel_stft.mel_scale.fb", "ema_model.mel_spec.mel_stft.spectrogram.window"]: + if key in checkpoint["ema_model_state_dict"]: + del checkpoint["ema_model_state_dict"][key] + + if self.is_main: + self.ema_model.load_state_dict(checkpoint["ema_model_state_dict"]) + + if "step" in checkpoint: + # patch for backward compatibility, 305e3ea + for key in ["mel_spec.mel_stft.mel_scale.fb", "mel_spec.mel_stft.spectrogram.window"]: + if key in checkpoint["model_state_dict"]: + del checkpoint["model_state_dict"][key] + + self.accelerator.unwrap_model(self.model).load_state_dict(checkpoint["model_state_dict"]) + self.accelerator.unwrap_model(self.optimizer).load_state_dict(checkpoint["optimizer_state_dict"]) + if self.scheduler: + self.scheduler.load_state_dict(checkpoint["scheduler_state_dict"]) + step = checkpoint["step"] + else: + checkpoint["model_state_dict"] = { + k.replace("ema_model.", ""): v + for k, v in checkpoint["ema_model_state_dict"].items() + if k not in ["initted", "step"] + } + self.accelerator.unwrap_model(self.model).load_state_dict(checkpoint["model_state_dict"]) + step = 0 + + del checkpoint + gc.collect() + return step + + def train(self, train_dataset: Dataset, num_workers=16, resumable_with_seed: int = None): + if self.log_samples: + from f5_tts.infer.utils_infer import cfg_strength, load_vocoder, nfe_step, sway_sampling_coef + + vocoder = load_vocoder(vocoder_name=self.vocoder_name) + target_sample_rate = self.accelerator.unwrap_model(self.model).mel_spec.target_sample_rate + log_samples_path = f"{self.checkpoint_path}/samples" + os.makedirs(log_samples_path, exist_ok=True) + + if exists(resumable_with_seed): + generator = torch.Generator() + generator.manual_seed(resumable_with_seed) + else: + generator = None + + if self.batch_size_type == "sample": + train_dataloader = DataLoader( + train_dataset, + collate_fn=collate_fn, + num_workers=num_workers, + pin_memory=True, + persistent_workers=True, + batch_size=self.batch_size, + shuffle=True, + generator=generator, + ) + elif self.batch_size_type == "frame": + self.accelerator.even_batches = False + sampler = SequentialSampler(train_dataset) + batch_sampler = DynamicBatchSampler( + sampler, self.batch_size, max_samples=self.max_samples, random_seed=resumable_with_seed, drop_last=False + ) + train_dataloader = DataLoader( + train_dataset, + collate_fn=collate_fn, + num_workers=num_workers, + pin_memory=True, + persistent_workers=True, + batch_sampler=batch_sampler, + ) + else: + raise ValueError(f"batch_size_type must be either 'sample' or 'frame', but received {self.batch_size_type}") + + # accelerator.prepare() dispatches batches to devices; + # which means the length of dataloader calculated before, should consider the number of devices + warmup_steps = ( + self.num_warmup_updates * self.accelerator.num_processes + ) # consider a fixed warmup steps while using accelerate multi-gpu ddp + # otherwise by default with split_batches=False, warmup steps change with num_processes + total_steps = len(train_dataloader) * self.epochs / self.grad_accumulation_steps + decay_steps = total_steps - warmup_steps + warmup_scheduler = LinearLR(self.optimizer, start_factor=1e-8, end_factor=1.0, total_iters=warmup_steps) + decay_scheduler = LinearLR(self.optimizer, start_factor=1.0, end_factor=1e-8, total_iters=decay_steps) + self.scheduler = SequentialLR( + self.optimizer, schedulers=[warmup_scheduler, decay_scheduler], milestones=[warmup_steps] + ) + train_dataloader, self.scheduler = self.accelerator.prepare( + train_dataloader, self.scheduler + ) # actual steps = 1 gpu steps / gpus + start_step = self.load_checkpoint() + global_step = start_step + + if exists(resumable_with_seed): + orig_epoch_step = len(train_dataloader) + skipped_epoch = int(start_step // orig_epoch_step) + skipped_batch = start_step % orig_epoch_step + skipped_dataloader = self.accelerator.skip_first_batches(train_dataloader, num_batches=skipped_batch) + else: + skipped_epoch = 0 + + for epoch in range(skipped_epoch, self.epochs): + self.model.train() + if exists(resumable_with_seed) and epoch == skipped_epoch: + progress_bar = tqdm( + skipped_dataloader, + desc=f"Epoch {epoch+1}/{self.epochs}", + unit="step", + disable=not self.accelerator.is_local_main_process, + initial=skipped_batch, + total=orig_epoch_step, + ) + else: + progress_bar = tqdm( + train_dataloader, + desc=f"Epoch {epoch+1}/{self.epochs}", + unit="step", + disable=not self.accelerator.is_local_main_process, + ) + + for batch in progress_bar: + with self.accelerator.accumulate(self.model): + text_inputs = batch["text"] + mel_spec = batch["mel"].permute(0, 2, 1) + mel_lengths = batch["mel_lengths"] + + # TODO. add duration predictor training + if self.duration_predictor is not None and self.accelerator.is_local_main_process: + dur_loss = self.duration_predictor(mel_spec, lens=batch.get("durations")) + self.accelerator.log({"duration loss": dur_loss.item()}, step=global_step) + + loss, cond, pred = self.model( + mel_spec, text=text_inputs, lens=mel_lengths, noise_scheduler=self.noise_scheduler + ) + self.accelerator.backward(loss) + + if self.max_grad_norm > 0 and self.accelerator.sync_gradients: + self.accelerator.clip_grad_norm_(self.model.parameters(), self.max_grad_norm) + + self.optimizer.step() + self.scheduler.step() + self.optimizer.zero_grad() + + if self.is_main: + self.ema_model.update() + + global_step += 1 + + if self.accelerator.is_local_main_process: + self.accelerator.log({"loss": loss.item(), "lr": self.scheduler.get_last_lr()[0]}, step=global_step) + if self.logger == "tensorboard": + self.writer.add_scalar("loss", loss.item(), global_step) + self.writer.add_scalar("lr", self.scheduler.get_last_lr()[0], global_step) + + progress_bar.set_postfix(step=str(global_step), loss=loss.item()) + + if global_step % (self.save_per_updates * self.grad_accumulation_steps) == 0: + self.save_checkpoint(global_step) + + if self.log_samples and self.accelerator.is_local_main_process: + ref_audio, ref_audio_len = vocoder.decode(batch["mel"][0].unsqueeze(0)), mel_lengths[0] + torchaudio.save( + f"{log_samples_path}/step_{global_step}_ref.wav", ref_audio.cpu(), target_sample_rate + ) + with torch.inference_mode(): + generated, _ = self.accelerator.unwrap_model(self.model).sample( + cond=mel_spec[0][:ref_audio_len].unsqueeze(0), + text=[text_inputs[0] + [" "] + text_inputs[0]], + duration=ref_audio_len * 2, + steps=nfe_step, + cfg_strength=cfg_strength, + sway_sampling_coef=sway_sampling_coef, + ) + generated = generated.to(torch.float32) + gen_audio = vocoder.decode( + generated[:, ref_audio_len:, :].permute(0, 2, 1).to(self.accelerator.device) + ) + torchaudio.save( + f"{log_samples_path}/step_{global_step}_gen.wav", gen_audio.cpu(), target_sample_rate + ) + + if global_step % self.last_per_steps == 0: + self.save_checkpoint(global_step, last=True) + + self.save_checkpoint(global_step, last=True) + + self.accelerator.end_training() diff --git a/src/f5_tts/model/utils.py b/src/f5_tts/model/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a98f00029c2c95358c5410ccadaff629aa1bd695 --- /dev/null +++ b/src/f5_tts/model/utils.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import os +import random +from collections import defaultdict +from importlib.resources import files + +import torch +from torch.nn.utils.rnn import pad_sequence + +import jieba +from pypinyin import lazy_pinyin, Style + + +# seed everything + + +def seed_everything(seed=0): + random.seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + +# helpers + + +def exists(v): + return v is not None + + +def default(v, d): + return v if exists(v) else d + + +# tensor helpers + + +def lens_to_mask(t: int["b"], length: int | None = None) -> bool["b n"]: # noqa: F722 F821 + if not exists(length): + length = t.amax() + + seq = torch.arange(length, device=t.device) + return seq[None, :] < t[:, None] + + +def mask_from_start_end_indices(seq_len: int["b"], start: int["b"], end: int["b"]): # noqa: F722 F821 + max_seq_len = seq_len.max().item() + seq = torch.arange(max_seq_len, device=start.device).long() + start_mask = seq[None, :] >= start[:, None] + end_mask = seq[None, :] < end[:, None] + return start_mask & end_mask + + +def mask_from_frac_lengths(seq_len: int["b"], frac_lengths: float["b"]): # noqa: F722 F821 + lengths = (frac_lengths * seq_len).long() + max_start = seq_len - lengths + + rand = torch.rand_like(frac_lengths) + start = (max_start * rand).long().clamp(min=0) + end = start + lengths + + return mask_from_start_end_indices(seq_len, start, end) + + +def maybe_masked_mean(t: float["b n d"], mask: bool["b n"] = None) -> float["b d"]: # noqa: F722 + if not exists(mask): + return t.mean(dim=1) + + t = torch.where(mask[:, :, None], t, torch.tensor(0.0, device=t.device)) + num = t.sum(dim=1) + den = mask.float().sum(dim=1) + + return num / den.clamp(min=1.0) + + +# simple utf-8 tokenizer, since paper went character based +def list_str_to_tensor(text: list[str], padding_value=-1) -> int["b nt"]: # noqa: F722 + list_tensors = [torch.tensor([*bytes(t, "UTF-8")]) for t in text] # ByT5 style + text = pad_sequence(list_tensors, padding_value=padding_value, batch_first=True) + return text + + +# char tokenizer, based on custom dataset's extracted .txt file +def list_str_to_idx( + text: list[str] | list[list[str]], + vocab_char_map: dict[str, int], # {char: idx} + padding_value=-1, +) -> int["b nt"]: # noqa: F722 + list_idx_tensors = [torch.tensor([vocab_char_map.get(c, 0) for c in t]) for t in text] # pinyin or char style + text = pad_sequence(list_idx_tensors, padding_value=padding_value, batch_first=True) + return text + + +# Get tokenizer + + +def get_tokenizer(dataset_name, tokenizer: str = "pinyin"): + """ + tokenizer - "pinyin" do g2p for only chinese characters, need .txt vocab_file + - "char" for char-wise tokenizer, need .txt vocab_file + - "byte" for utf-8 tokenizer + - "custom" if you're directly passing in a path to the vocab.txt you want to use + vocab_size - if use "pinyin", all available pinyin types, common alphabets (also those with accent) and symbols + - if use "char", derived from unfiltered character & symbol counts of custom dataset + - if use "byte", set to 256 (unicode byte range) + """ + if tokenizer in ["pinyin", "char"]: + tokenizer_path = os.path.join(files("f5_tts").joinpath("../../data"), f"{dataset_name}_{tokenizer}/vocab.txt") + with open(tokenizer_path, "r", encoding="utf-8") as f: + vocab_char_map = {} + for i, char in enumerate(f): + vocab_char_map[char[:-1]] = i + vocab_size = len(vocab_char_map) + assert vocab_char_map[" "] == 0, "make sure space is of idx 0 in vocab.txt, cuz 0 is used for unknown char" + + elif tokenizer == "byte": + vocab_char_map = None + vocab_size = 256 + + elif tokenizer == "custom": + with open(dataset_name, "r", encoding="utf-8") as f: + vocab_char_map = {} + for i, char in enumerate(f): + vocab_char_map[char[:-1]] = i + vocab_size = len(vocab_char_map) + + return vocab_char_map, vocab_size + + +# convert char to pinyin + + +def convert_char_to_pinyin(text_list, polyphone=True): + final_text_list = [] + god_knows_why_en_testset_contains_zh_quote = str.maketrans( + {"“": '"', "”": '"', "‘": "'", "’": "'"} + ) # in case librispeech (orig no-pc) test-clean + custom_trans = str.maketrans({";": ","}) # add custom trans here, to address oov + for text in text_list: + char_list = [] + text = text.translate(god_knows_why_en_testset_contains_zh_quote) + text = text.translate(custom_trans) + for seg in jieba.cut(text): + seg_byte_len = len(bytes(seg, "UTF-8")) + if seg_byte_len == len(seg): # if pure alphabets and symbols + if char_list and seg_byte_len > 1 and char_list[-1] not in " :'\"": + char_list.append(" ") + char_list.extend(seg) + elif polyphone and seg_byte_len == 3 * len(seg): # if pure chinese characters + seg = lazy_pinyin(seg, style=Style.TONE3, tone_sandhi=True) + for c in seg: + if c not in "。,、;:?!《》【】—…": + char_list.append(" ") + char_list.append(c) + else: # if mixed chinese characters, alphabets and symbols + for c in seg: + if ord(c) < 256: + char_list.extend(c) + else: + if c not in "。,、;:?!《》【】—…": + char_list.append(" ") + char_list.extend(lazy_pinyin(c, style=Style.TONE3, tone_sandhi=True)) + else: # if is zh punc + char_list.append(c) + final_text_list.append(char_list) + + return final_text_list + + +# filter func for dirty data with many repetitions + + +def repetition_found(text, length=2, tolerance=10): + pattern_count = defaultdict(int) + for i in range(len(text) - length + 1): + pattern = text[i : i + length] + pattern_count[pattern] += 1 + for pattern, count in pattern_count.items(): + if count > tolerance: + return True + return False diff --git a/src/f5_tts/scripts/count_max_epoch.py b/src/f5_tts/scripts/count_max_epoch.py new file mode 100644 index 0000000000000000000000000000000000000000..593afe50f10c2eb71bfe552f6923c46a221c7af8 --- /dev/null +++ b/src/f5_tts/scripts/count_max_epoch.py @@ -0,0 +1,33 @@ +"""ADAPTIVE BATCH SIZE""" + +print("Adaptive batch size: using grouping batch sampler, frames_per_gpu fixed fed in") +print(" -> least padding, gather wavs with accumulated frames in a batch\n") + +# data +total_hours = 95282 +mel_hop_length = 256 +mel_sampling_rate = 24000 + +# target +wanted_max_updates = 1000000 + +# train params +gpus = 8 +frames_per_gpu = 38400 # 8 * 38400 = 307200 +grad_accum = 1 + +# intermediate +mini_batch_frames = frames_per_gpu * grad_accum * gpus +mini_batch_hours = mini_batch_frames * mel_hop_length / mel_sampling_rate / 3600 +updates_per_epoch = total_hours / mini_batch_hours +steps_per_epoch = updates_per_epoch * grad_accum + +# result +epochs = wanted_max_updates / updates_per_epoch +print(f"epochs should be set to: {epochs:.0f} ({epochs/grad_accum:.1f} x gd_acum {grad_accum})") +print(f"progress_bar should show approx. 0/{updates_per_epoch:.0f} updates") +print(f" or approx. 0/{steps_per_epoch:.0f} steps") + +# others +print(f"total {total_hours:.0f} hours") +print(f"mini-batch of {mini_batch_frames:.0f} frames, {mini_batch_hours:.2f} hours per mini-batch") diff --git a/src/f5_tts/scripts/count_params_gflops.py b/src/f5_tts/scripts/count_params_gflops.py new file mode 100644 index 0000000000000000000000000000000000000000..58f68dabe238006bea17138adfa9e60eb60b0cb9 --- /dev/null +++ b/src/f5_tts/scripts/count_params_gflops.py @@ -0,0 +1,39 @@ +import sys +import os + +sys.path.append(os.getcwd()) + +from f5_tts.model import CFM, DiT + +import torch +import thop + + +""" ~155M """ +# transformer = UNetT(dim = 768, depth = 20, heads = 12, ff_mult = 4) +# transformer = UNetT(dim = 768, depth = 20, heads = 12, ff_mult = 4, text_dim = 512, conv_layers = 4) +# transformer = DiT(dim = 768, depth = 18, heads = 12, ff_mult = 2) +# transformer = DiT(dim = 768, depth = 18, heads = 12, ff_mult = 2, text_dim = 512, conv_layers = 4) +# transformer = DiT(dim = 768, depth = 18, heads = 12, ff_mult = 2, text_dim = 512, conv_layers = 4, long_skip_connection = True) +# transformer = MMDiT(dim = 512, depth = 16, heads = 16, ff_mult = 2) + +""" ~335M """ +# FLOPs: 622.1 G, Params: 333.2 M +# transformer = UNetT(dim = 1024, depth = 24, heads = 16, ff_mult = 4) +# FLOPs: 363.4 G, Params: 335.8 M +transformer = DiT(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + + +model = CFM(transformer=transformer) +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 +duration = 20 +frame_length = int(duration * target_sample_rate / hop_length) +text_length = 150 + +flops, params = thop.profile( + model, inputs=(torch.randn(1, frame_length, n_mel_channels), torch.zeros(1, text_length, dtype=torch.long)) +) +print(f"FLOPs: {flops / 1e9} G") +print(f"Params: {params / 1e6} M") diff --git a/src/f5_tts/socket_server.py b/src/f5_tts/socket_server.py new file mode 100644 index 0000000000000000000000000000000000000000..85dd2709bec22f2c1dbbb3f3127a54bae02d5b40 --- /dev/null +++ b/src/f5_tts/socket_server.py @@ -0,0 +1,163 @@ +import socket +import struct +import torch +import torchaudio +from threading import Thread + + +import gc +import traceback + + +from infer.utils_infer import infer_batch_process, preprocess_ref_audio_text, load_vocoder, load_model +from model.backbones.dit import DiT + + +class TTSStreamingProcessor: + def __init__(self, ckpt_file, vocab_file, ref_audio, ref_text, device=None, dtype=torch.float32): + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + + # Load the model using the provided checkpoint and vocab files + self.model = load_model( + model_cls=DiT, + model_cfg=dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4), + ckpt_path=ckpt_file, + mel_spec_type="vocos", # or "bigvgan" depending on vocoder + vocab_file=vocab_file, + ode_method="euler", + use_ema=True, + device=self.device, + ).to(self.device, dtype=dtype) + + # Load the vocoder + self.vocoder = load_vocoder(is_local=False) + + # Set sampling rate for streaming + self.sampling_rate = 24000 # Consistency with client + + # Set reference audio and text + self.ref_audio = ref_audio + self.ref_text = ref_text + + # Warm up the model + self._warm_up() + + def _warm_up(self): + """Warm up the model with a dummy input to ensure it's ready for real-time processing.""" + print("Warming up the model...") + ref_audio, ref_text = preprocess_ref_audio_text(self.ref_audio, self.ref_text) + audio, sr = torchaudio.load(ref_audio) + gen_text = "Warm-up text for the model." + + # Pass the vocoder as an argument here + infer_batch_process((audio, sr), ref_text, [gen_text], self.model, self.vocoder, device=self.device) + print("Warm-up completed.") + + def generate_stream(self, text, play_steps_in_s=0.5): + """Generate audio in chunks and yield them in real-time.""" + # Preprocess the reference audio and text + ref_audio, ref_text = preprocess_ref_audio_text(self.ref_audio, self.ref_text) + + # Load reference audio + audio, sr = torchaudio.load(ref_audio) + + # Run inference for the input text + audio_chunk, final_sample_rate, _ = infer_batch_process( + (audio, sr), + ref_text, + [text], + self.model, + self.vocoder, + device=self.device, # Pass vocoder here + ) + + # Break the generated audio into chunks and send them + chunk_size = int(final_sample_rate * play_steps_in_s) + + for i in range(0, len(audio_chunk), chunk_size): + chunk = audio_chunk[i : i + chunk_size] + + # Check if it's the final chunk + if i + chunk_size >= len(audio_chunk): + chunk = audio_chunk[i:] + + # Avoid sending empty or repeated chunks + if len(chunk) == 0: + break + + # Pack and send the audio chunk + packed_audio = struct.pack(f"{len(chunk)}f", *chunk) + yield packed_audio + + # Ensure that no final word is repeated by not resending partial chunks + if len(audio_chunk) % chunk_size != 0: + remaining_chunk = audio_chunk[-(len(audio_chunk) % chunk_size) :] + packed_audio = struct.pack(f"{len(remaining_chunk)}f", *remaining_chunk) + yield packed_audio + + +def handle_client(client_socket, processor): + try: + while True: + # Receive data from the client + data = client_socket.recv(1024).decode("utf-8") + if not data: + break + + try: + # The client sends the text input + text = data.strip() + + # Generate and stream audio chunks + for audio_chunk in processor.generate_stream(text): + client_socket.sendall(audio_chunk) + + # Send end-of-audio signal + client_socket.sendall(b"END_OF_AUDIO") + + except Exception as inner_e: + print(f"Error during processing: {inner_e}") + traceback.print_exc() # Print the full traceback to diagnose the issue + break + + except Exception as e: + print(f"Error handling client: {e}") + traceback.print_exc() + finally: + client_socket.close() + + +def start_server(host, port, processor): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind((host, port)) + server.listen(5) + print(f"Server listening on {host}:{port}") + + while True: + client_socket, addr = server.accept() + print(f"Accepted connection from {addr}") + client_handler = Thread(target=handle_client, args=(client_socket, processor)) + client_handler.start() + + +if __name__ == "__main__": + try: + # Load the model and vocoder using the provided files + ckpt_file = "" # pointing your checkpoint "ckpts/model/model_1096.pt" + vocab_file = "" # Add vocab file path if needed + ref_audio = "" # add ref audio"./tests/ref_audio/reference.wav" + ref_text = "" + + # Initialize the processor with the model and vocoder + processor = TTSStreamingProcessor( + ckpt_file=ckpt_file, + vocab_file=vocab_file, + ref_audio=ref_audio, + ref_text=ref_text, + dtype=torch.float32, + ) + + # Start the server + start_server("0.0.0.0", 9998, processor) + except KeyboardInterrupt: + gc.collect() diff --git a/src/f5_tts/train/README.md b/src/f5_tts/train/README.md new file mode 100644 index 0000000000000000000000000000000000000000..89ce7a92c3ac388dc8368b315441966f22a2934d --- /dev/null +++ b/src/f5_tts/train/README.md @@ -0,0 +1,69 @@ +# Training + +## Prepare Dataset + +Example data processing scripts for Emilia and Wenetspeech4TTS, and you may tailor your own one along with a Dataset class in `src/f5_tts/model/dataset.py`. + +### 1. Datasets used for pretrained models +Download corresponding dataset first, and fill in the path in scripts. + +```bash +# Prepare the Emilia dataset +python src/f5_tts/train/datasets/prepare_emilia.py + +# Prepare the Wenetspeech4TTS dataset +python src/f5_tts/train/datasets/prepare_wenetspeech4tts.py +``` + +### 2. Create custom dataset with metadata.csv +Use guidance see [#57 here](https://github.com/SWivid/F5-TTS/discussions/57#discussioncomment-10959029). + +```bash +python src/f5_tts/train/datasets/prepare_csv_wavs.py +``` + +## Training & Finetuning + +Once your datasets are prepared, you can start the training process. + +### 1. Training script used for pretrained model + +```bash +# setup accelerate config, e.g. use multi-gpu ddp, fp16 +# will be to: ~/.cache/huggingface/accelerate/default_config.yaml +accelerate config +accelerate launch src/f5_tts/train/train.py +``` + +### 2. Finetuning practice +Discussion board for Finetuning [#57](https://github.com/SWivid/F5-TTS/discussions/57). + +Gradio UI training/finetuning with `src/f5_tts/train/finetune_gradio.py` see [#143](https://github.com/SWivid/F5-TTS/discussions/143). + +### 3. Wandb Logging + +The `wandb/` dir will be created under path you run training/finetuning scripts. + +By default, the training script does NOT use logging (assuming you didn't manually log in using `wandb login`). + +To turn on wandb logging, you can either: + +1. Manually login with `wandb login`: Learn more [here](https://docs.wandb.ai/ref/cli/wandb-login) +2. Automatically login programmatically by setting an environment variable: Get an API KEY at https://wandb.ai/site/ and set the environment variable as follows: + +On Mac & Linux: + +``` +export WANDB_API_KEY= +``` + +On Windows: + +``` +set WANDB_API_KEY= +``` +Moreover, if you couldn't access Wandb and want to log metrics offline, you can the environment variable as follows: + +``` +export WANDB_MODE=offline +``` diff --git a/src/f5_tts/train/datasets/prepare_csv_wavs.py b/src/f5_tts/train/datasets/prepare_csv_wavs.py new file mode 100644 index 0000000000000000000000000000000000000000..1c8f8dd7c33a1a36a9deea4e15d1ae50a8f6eeb1 --- /dev/null +++ b/src/f5_tts/train/datasets/prepare_csv_wavs.py @@ -0,0 +1,139 @@ +import os +import sys + +sys.path.append(os.getcwd()) + +import argparse +import csv +import json +import shutil +from importlib.resources import files +from pathlib import Path + +import torchaudio +from tqdm import tqdm +from datasets.arrow_writer import ArrowWriter + +from f5_tts.model.utils import ( + convert_char_to_pinyin, +) + + +PRETRAINED_VOCAB_PATH = files("f5_tts").joinpath("../../data/Emilia_ZH_EN_pinyin/vocab.txt") + + +def is_csv_wavs_format(input_dataset_dir): + fpath = Path(input_dataset_dir) + metadata = fpath / "metadata.csv" + wavs = fpath / "wavs" + return metadata.exists() and metadata.is_file() and wavs.exists() and wavs.is_dir() + + +def prepare_csv_wavs_dir(input_dir): + assert is_csv_wavs_format(input_dir), f"not csv_wavs format: {input_dir}" + input_dir = Path(input_dir) + metadata_path = input_dir / "metadata.csv" + audio_path_text_pairs = read_audio_text_pairs(metadata_path.as_posix()) + + sub_result, durations = [], [] + vocab_set = set() + polyphone = True + for audio_path, text in audio_path_text_pairs: + if not Path(audio_path).exists(): + print(f"audio {audio_path} not found, skipping") + continue + audio_duration = get_audio_duration(audio_path) + # assume tokenizer = "pinyin" ("pinyin" | "char") + text = convert_char_to_pinyin([text], polyphone=polyphone)[0] + sub_result.append({"audio_path": audio_path, "text": text, "duration": audio_duration}) + durations.append(audio_duration) + vocab_set.update(list(text)) + + return sub_result, durations, vocab_set + + +def get_audio_duration(audio_path): + audio, sample_rate = torchaudio.load(audio_path) + return audio.shape[1] / sample_rate + + +def read_audio_text_pairs(csv_file_path): + audio_text_pairs = [] + + parent = Path(csv_file_path).parent + with open(csv_file_path, mode="r", newline="", encoding="utf-8-sig") as csvfile: + reader = csv.reader(csvfile, delimiter="|") + next(reader) # Skip the header row + for row in reader: + if len(row) >= 2: + audio_file = row[0].strip() # First column: audio file path + text = row[1].strip() # Second column: text + audio_file_path = parent / audio_file + audio_text_pairs.append((audio_file_path.as_posix(), text)) + + return audio_text_pairs + + +def save_prepped_dataset(out_dir, result, duration_list, text_vocab_set, is_finetune): + out_dir = Path(out_dir) + # save preprocessed dataset to disk + out_dir.mkdir(exist_ok=True, parents=True) + print(f"\nSaving to {out_dir} ...") + + # dataset = Dataset.from_dict({"audio_path": audio_path_list, "text": text_list, "duration": duration_list}) # oom + # dataset.save_to_disk(f"{out_dir}/raw", max_shard_size="2GB") + raw_arrow_path = out_dir / "raw.arrow" + with ArrowWriter(path=raw_arrow_path.as_posix(), writer_batch_size=1) as writer: + for line in tqdm(result, desc="Writing to raw.arrow ..."): + writer.write(line) + + # dup a json separately saving duration in case for DynamicBatchSampler ease + dur_json_path = out_dir / "duration.json" + with open(dur_json_path.as_posix(), "w", encoding="utf-8") as f: + json.dump({"duration": duration_list}, f, ensure_ascii=False) + + # vocab map, i.e. tokenizer + # add alphabets and symbols (optional, if plan to ft on de/fr etc.) + # if tokenizer == "pinyin": + # text_vocab_set.update([chr(i) for i in range(32, 127)] + [chr(i) for i in range(192, 256)]) + voca_out_path = out_dir / "vocab.txt" + with open(voca_out_path.as_posix(), "w") as f: + for vocab in sorted(text_vocab_set): + f.write(vocab + "\n") + + if is_finetune: + file_vocab_finetune = PRETRAINED_VOCAB_PATH.as_posix() + shutil.copy2(file_vocab_finetune, voca_out_path) + else: + with open(voca_out_path, "w") as f: + for vocab in sorted(text_vocab_set): + f.write(vocab + "\n") + + dataset_name = out_dir.stem + print(f"\nFor {dataset_name}, sample count: {len(result)}") + print(f"For {dataset_name}, vocab size is: {len(text_vocab_set)}") + print(f"For {dataset_name}, total {sum(duration_list)/3600:.2f} hours") + + +def prepare_and_save_set(inp_dir, out_dir, is_finetune: bool = True): + if is_finetune: + assert PRETRAINED_VOCAB_PATH.exists(), f"pretrained vocab.txt not found: {PRETRAINED_VOCAB_PATH}" + sub_result, durations, vocab_set = prepare_csv_wavs_dir(inp_dir) + save_prepped_dataset(out_dir, sub_result, durations, vocab_set, is_finetune) + + +def cli(): + # finetune: python scripts/prepare_csv_wavs.py /path/to/input_dir /path/to/output_dir_pinyin + # pretrain: python scripts/prepare_csv_wavs.py /path/to/output_dir_pinyin --pretrain + parser = argparse.ArgumentParser(description="Prepare and save dataset.") + parser.add_argument("inp_dir", type=str, help="Input directory containing the data.") + parser.add_argument("out_dir", type=str, help="Output directory to save the prepared data.") + parser.add_argument("--pretrain", action="store_true", help="Enable for new pretrain, otherwise is a fine-tune") + + args = parser.parse_args() + + prepare_and_save_set(args.inp_dir, args.out_dir, is_finetune=not args.pretrain) + + +if __name__ == "__main__": + cli() diff --git a/src/f5_tts/train/datasets/prepare_emilia.py b/src/f5_tts/train/datasets/prepare_emilia.py new file mode 100644 index 0000000000000000000000000000000000000000..aabd549c5b06f60ed31fb44acba0336080de7339 --- /dev/null +++ b/src/f5_tts/train/datasets/prepare_emilia.py @@ -0,0 +1,230 @@ +# Emilia Dataset: https://huggingface.co/datasets/amphion/Emilia-Dataset/tree/fc71e07 +# if use updated new version, i.e. WebDataset, feel free to modify / draft your own script + +# generate audio text map for Emilia ZH & EN +# evaluate for vocab size + +import os +import sys + +sys.path.append(os.getcwd()) + +import json +from concurrent.futures import ProcessPoolExecutor +from importlib.resources import files +from pathlib import Path +from tqdm import tqdm + +from datasets.arrow_writer import ArrowWriter + +from f5_tts.model.utils import ( + repetition_found, + convert_char_to_pinyin, +) + + +out_zh = { + "ZH_B00041_S06226", + "ZH_B00042_S09204", + "ZH_B00065_S09430", + "ZH_B00065_S09431", + "ZH_B00066_S09327", + "ZH_B00066_S09328", +} +zh_filters = ["い", "て"] +# seems synthesized audios, or heavily code-switched +out_en = { + "EN_B00013_S00913", + "EN_B00042_S00120", + "EN_B00055_S04111", + "EN_B00061_S00693", + "EN_B00061_S01494", + "EN_B00061_S03375", + "EN_B00059_S00092", + "EN_B00111_S04300", + "EN_B00100_S03759", + "EN_B00087_S03811", + "EN_B00059_S00950", + "EN_B00089_S00946", + "EN_B00078_S05127", + "EN_B00070_S04089", + "EN_B00074_S09659", + "EN_B00061_S06983", + "EN_B00061_S07060", + "EN_B00059_S08397", + "EN_B00082_S06192", + "EN_B00091_S01238", + "EN_B00089_S07349", + "EN_B00070_S04343", + "EN_B00061_S02400", + "EN_B00076_S01262", + "EN_B00068_S06467", + "EN_B00076_S02943", + "EN_B00064_S05954", + "EN_B00061_S05386", + "EN_B00066_S06544", + "EN_B00076_S06944", + "EN_B00072_S08620", + "EN_B00076_S07135", + "EN_B00076_S09127", + "EN_B00065_S00497", + "EN_B00059_S06227", + "EN_B00063_S02859", + "EN_B00075_S01547", + "EN_B00061_S08286", + "EN_B00079_S02901", + "EN_B00092_S03643", + "EN_B00096_S08653", + "EN_B00063_S04297", + "EN_B00063_S04614", + "EN_B00079_S04698", + "EN_B00104_S01666", + "EN_B00061_S09504", + "EN_B00061_S09694", + "EN_B00065_S05444", + "EN_B00063_S06860", + "EN_B00065_S05725", + "EN_B00069_S07628", + "EN_B00083_S03875", + "EN_B00071_S07665", + "EN_B00071_S07665", + "EN_B00062_S04187", + "EN_B00065_S09873", + "EN_B00065_S09922", + "EN_B00084_S02463", + "EN_B00067_S05066", + "EN_B00106_S08060", + "EN_B00073_S06399", + "EN_B00073_S09236", + "EN_B00087_S00432", + "EN_B00085_S05618", + "EN_B00064_S01262", + "EN_B00072_S01739", + "EN_B00059_S03913", + "EN_B00069_S04036", + "EN_B00067_S05623", + "EN_B00060_S05389", + "EN_B00060_S07290", + "EN_B00062_S08995", +} +en_filters = ["ا", "い", "て"] + + +def deal_with_audio_dir(audio_dir): + audio_jsonl = audio_dir.with_suffix(".jsonl") + sub_result, durations = [], [] + vocab_set = set() + bad_case_zh = 0 + bad_case_en = 0 + with open(audio_jsonl, "r") as f: + lines = f.readlines() + for line in tqdm(lines, desc=f"{audio_jsonl.stem}"): + obj = json.loads(line) + text = obj["text"] + if obj["language"] == "zh": + if obj["wav"].split("/")[1] in out_zh or any(f in text for f in zh_filters) or repetition_found(text): + bad_case_zh += 1 + continue + else: + text = text.translate( + str.maketrans({",": ",", "!": "!", "?": "?"}) + ) # not "。" cuz much code-switched + if obj["language"] == "en": + if ( + obj["wav"].split("/")[1] in out_en + or any(f in text for f in en_filters) + or repetition_found(text, length=4) + ): + bad_case_en += 1 + continue + if tokenizer == "pinyin": + text = convert_char_to_pinyin([text], polyphone=polyphone)[0] + duration = obj["duration"] + sub_result.append({"audio_path": str(audio_dir.parent / obj["wav"]), "text": text, "duration": duration}) + durations.append(duration) + vocab_set.update(list(text)) + return sub_result, durations, vocab_set, bad_case_zh, bad_case_en + + +def main(): + assert tokenizer in ["pinyin", "char"] + result = [] + duration_list = [] + text_vocab_set = set() + total_bad_case_zh = 0 + total_bad_case_en = 0 + + # process raw data + executor = ProcessPoolExecutor(max_workers=max_workers) + futures = [] + for lang in langs: + dataset_path = Path(os.path.join(dataset_dir, lang)) + [ + futures.append(executor.submit(deal_with_audio_dir, audio_dir)) + for audio_dir in dataset_path.iterdir() + if audio_dir.is_dir() + ] + for futures in tqdm(futures, total=len(futures)): + sub_result, durations, vocab_set, bad_case_zh, bad_case_en = futures.result() + result.extend(sub_result) + duration_list.extend(durations) + text_vocab_set.update(vocab_set) + total_bad_case_zh += bad_case_zh + total_bad_case_en += bad_case_en + executor.shutdown() + + # save preprocessed dataset to disk + if not os.path.exists(f"{save_dir}"): + os.makedirs(f"{save_dir}") + print(f"\nSaving to {save_dir} ...") + + # dataset = Dataset.from_dict({"audio_path": audio_path_list, "text": text_list, "duration": duration_list}) # oom + # dataset.save_to_disk(f"{save_dir}/raw", max_shard_size="2GB") + with ArrowWriter(path=f"{save_dir}/raw.arrow") as writer: + for line in tqdm(result, desc="Writing to raw.arrow ..."): + writer.write(line) + + # dup a json separately saving duration in case for DynamicBatchSampler ease + with open(f"{save_dir}/duration.json", "w", encoding="utf-8") as f: + json.dump({"duration": duration_list}, f, ensure_ascii=False) + + # vocab map, i.e. tokenizer + # add alphabets and symbols (optional, if plan to ft on de/fr etc.) + # if tokenizer == "pinyin": + # text_vocab_set.update([chr(i) for i in range(32, 127)] + [chr(i) for i in range(192, 256)]) + with open(f"{save_dir}/vocab.txt", "w") as f: + for vocab in sorted(text_vocab_set): + f.write(vocab + "\n") + + print(f"\nFor {dataset_name}, sample count: {len(result)}") + print(f"For {dataset_name}, vocab size is: {len(text_vocab_set)}") + print(f"For {dataset_name}, total {sum(duration_list)/3600:.2f} hours") + if "ZH" in langs: + print(f"Bad zh transcription case: {total_bad_case_zh}") + if "EN" in langs: + print(f"Bad en transcription case: {total_bad_case_en}\n") + + +if __name__ == "__main__": + max_workers = 32 + + tokenizer = "pinyin" # "pinyin" | "char" + polyphone = True + + langs = ["ZH", "EN"] + dataset_dir = "/Emilia_Dataset/raw" + dataset_name = f"Emilia_{'_'.join(langs)}_{tokenizer}" + save_dir = str(files("f5_tts").joinpath("../../")) + f"/data/{dataset_name}" + print(f"\nPrepare for {dataset_name}, will save to {save_dir}\n") + + main() + + # Emilia ZH & EN + # samples count 37837916 (after removal) + # pinyin vocab size 2543 (polyphone) + # total duration 95281.87 (hours) + # bad zh asr cnt 230435 (samples) + # bad eh asr cnt 37217 (samples) + + # vocab size may be slightly different due to jieba tokenizer and pypinyin (e.g. way of polyphoneme) + # please be careful if using pretrained model, make sure the vocab.txt is same diff --git a/src/f5_tts/train/datasets/prepare_wenetspeech4tts.py b/src/f5_tts/train/datasets/prepare_wenetspeech4tts.py new file mode 100644 index 0000000000000000000000000000000000000000..7f29bbe3b148025733ba2d6b4bdfdf1e9d4b965b --- /dev/null +++ b/src/f5_tts/train/datasets/prepare_wenetspeech4tts.py @@ -0,0 +1,125 @@ +# generate audio text map for WenetSpeech4TTS +# evaluate for vocab size + +import os +import sys + +sys.path.append(os.getcwd()) + +import json +from concurrent.futures import ProcessPoolExecutor +from importlib.resources import files +from tqdm import tqdm + +import torchaudio +from datasets import Dataset + +from f5_tts.model.utils import convert_char_to_pinyin + + +def deal_with_sub_path_files(dataset_path, sub_path): + print(f"Dealing with: {sub_path}") + + text_dir = os.path.join(dataset_path, sub_path, "txts") + audio_dir = os.path.join(dataset_path, sub_path, "wavs") + text_files = os.listdir(text_dir) + + audio_paths, texts, durations = [], [], [] + for text_file in tqdm(text_files): + with open(os.path.join(text_dir, text_file), "r", encoding="utf-8") as file: + first_line = file.readline().split("\t") + audio_nm = first_line[0] + audio_path = os.path.join(audio_dir, audio_nm + ".wav") + text = first_line[1].strip() + + audio_paths.append(audio_path) + + if tokenizer == "pinyin": + texts.extend(convert_char_to_pinyin([text], polyphone=polyphone)) + elif tokenizer == "char": + texts.append(text) + + audio, sample_rate = torchaudio.load(audio_path) + durations.append(audio.shape[-1] / sample_rate) + + return audio_paths, texts, durations + + +def main(): + assert tokenizer in ["pinyin", "char"] + + audio_path_list, text_list, duration_list = [], [], [] + + executor = ProcessPoolExecutor(max_workers=max_workers) + futures = [] + for dataset_path in dataset_paths: + sub_items = os.listdir(dataset_path) + sub_paths = [item for item in sub_items if os.path.isdir(os.path.join(dataset_path, item))] + for sub_path in sub_paths: + futures.append(executor.submit(deal_with_sub_path_files, dataset_path, sub_path)) + for future in tqdm(futures, total=len(futures)): + audio_paths, texts, durations = future.result() + audio_path_list.extend(audio_paths) + text_list.extend(texts) + duration_list.extend(durations) + executor.shutdown() + + if not os.path.exists("data"): + os.makedirs("data") + + print(f"\nSaving to {save_dir} ...") + dataset = Dataset.from_dict({"audio_path": audio_path_list, "text": text_list, "duration": duration_list}) + dataset.save_to_disk(f"{save_dir}/raw", max_shard_size="2GB") # arrow format + + with open(f"{save_dir}/duration.json", "w", encoding="utf-8") as f: + json.dump( + {"duration": duration_list}, f, ensure_ascii=False + ) # dup a json separately saving duration in case for DynamicBatchSampler ease + + print("\nEvaluating vocab size (all characters and symbols / all phonemes) ...") + text_vocab_set = set() + for text in tqdm(text_list): + text_vocab_set.update(list(text)) + + # add alphabets and symbols (optional, if plan to ft on de/fr etc.) + if tokenizer == "pinyin": + text_vocab_set.update([chr(i) for i in range(32, 127)] + [chr(i) for i in range(192, 256)]) + + with open(f"{save_dir}/vocab.txt", "w") as f: + for vocab in sorted(text_vocab_set): + f.write(vocab + "\n") + print(f"\nFor {dataset_name}, sample count: {len(text_list)}") + print(f"For {dataset_name}, vocab size is: {len(text_vocab_set)}\n") + + +if __name__ == "__main__": + max_workers = 32 + + tokenizer = "pinyin" # "pinyin" | "char" + polyphone = True + dataset_choice = 1 # 1: Premium, 2: Standard, 3: Basic + + dataset_name = ( + ["WenetSpeech4TTS_Premium", "WenetSpeech4TTS_Standard", "WenetSpeech4TTS_Basic"][dataset_choice - 1] + + "_" + + tokenizer + ) + dataset_paths = [ + "/WenetSpeech4TTS/Basic", + "/WenetSpeech4TTS/Standard", + "/WenetSpeech4TTS/Premium", + ][-dataset_choice:] + save_dir = str(files("f5_tts").joinpath("../../")) + f"/data/{dataset_name}" + print(f"\nChoose Dataset: {dataset_name}, will save to {save_dir}\n") + + main() + + # Results (if adding alphabets with accents and symbols): + # WenetSpeech4TTS Basic Standard Premium + # samples count 3932473 1941220 407494 + # pinyin vocab size 1349 1348 1344 (no polyphone) + # - - 1459 (polyphone) + # char vocab size 5264 5219 5042 + + # vocab size may be slightly different due to jieba tokenizer and pypinyin (e.g. way of polyphoneme) + # please be careful if using pretrained model, make sure the vocab.txt is same diff --git a/src/f5_tts/train/finetune_cli.py b/src/f5_tts/train/finetune_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..579fd695be594bb5542bc88d13bb61f02712b4fe --- /dev/null +++ b/src/f5_tts/train/finetune_cli.py @@ -0,0 +1,161 @@ +import argparse +import os +import shutil + +from cached_path import cached_path +from f5_tts.model import CFM, UNetT, DiT, Trainer +from f5_tts.model.utils import get_tokenizer +from f5_tts.model.dataset import load_dataset +from importlib.resources import files + + +# -------------------------- Dataset Settings --------------------------- # +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 + + +# -------------------------- Argument Parsing --------------------------- # +def parse_args(): + # batch_size_per_gpu = 1000 settting for gpu 8GB + # batch_size_per_gpu = 1600 settting for gpu 12GB + # batch_size_per_gpu = 2000 settting for gpu 16GB + # batch_size_per_gpu = 3200 settting for gpu 24GB + + # num_warmup_updates = 300 for 5000 sample about 10 hours + + # change save_per_updates , last_per_steps change this value what you need , + + parser = argparse.ArgumentParser(description="Train CFM Model") + + parser.add_argument( + "--exp_name", type=str, default="F5TTS_Base", choices=["F5TTS_Base", "E2TTS_Base"], help="Experiment name" + ) + parser.add_argument("--dataset_name", type=str, default="Emilia_ZH_EN", help="Name of the dataset to use") + parser.add_argument("--learning_rate", type=float, default=1e-5, help="Learning rate for training") + parser.add_argument("--batch_size_per_gpu", type=int, default=3200, help="Batch size per GPU") + parser.add_argument( + "--batch_size_type", type=str, default="frame", choices=["frame", "sample"], help="Batch size type" + ) + parser.add_argument("--max_samples", type=int, default=64, help="Max sequences per batch") + parser.add_argument("--grad_accumulation_steps", type=int, default=1, help="Gradient accumulation steps") + parser.add_argument("--max_grad_norm", type=float, default=1.0, help="Max gradient norm for clipping") + parser.add_argument("--epochs", type=int, default=10, help="Number of training epochs") + parser.add_argument("--num_warmup_updates", type=int, default=300, help="Warmup steps") + parser.add_argument("--save_per_updates", type=int, default=10000, help="Save checkpoint every X steps") + parser.add_argument("--last_per_steps", type=int, default=50000, help="Save last checkpoint every X steps") + parser.add_argument("--finetune", type=bool, default=True, help="Use Finetune") + parser.add_argument("--pretrain", type=str, default=None, help="the path to the checkpoint") + parser.add_argument( + "--tokenizer", type=str, default="pinyin", choices=["pinyin", "char", "custom"], help="Tokenizer type" + ) + parser.add_argument( + "--tokenizer_path", + type=str, + default=None, + help="Path to custom tokenizer vocab file (only used if tokenizer = 'custom')", + ) + + parser.add_argument( + "--log_samples", + type=bool, + default=False, + help="Log inferenced samples per ckpt save steps", + ) + parser.add_argument("--logger", type=str, default=None, choices=["wandb", "tensorboard"], help="logger") + + return parser.parse_args() + + +# -------------------------- Training Settings -------------------------- # + + +def main(): + args = parse_args() + + checkpoint_path = str(files("f5_tts").joinpath(f"../../ckpts/{args.dataset_name}")) + + # Model parameters based on experiment name + if args.exp_name == "F5TTS_Base": + wandb_resume_id = None + model_cls = DiT + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) + if args.finetune: + if args.pretrain is None: + ckpt_path = str(cached_path("hf://SWivid/F5-TTS/F5TTS_Base/model_1200000.pt")) + else: + ckpt_path = args.pretrain + elif args.exp_name == "E2TTS_Base": + wandb_resume_id = None + model_cls = UNetT + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + if args.finetune: + if args.pretrain is None: + ckpt_path = str(cached_path("hf://SWivid/E2-TTS/E2TTS_Base/model_1200000.pt")) + else: + ckpt_path = args.pretrain + + if args.finetune: + if not os.path.isdir(checkpoint_path): + os.makedirs(checkpoint_path, exist_ok=True) + + file_checkpoint = os.path.join(checkpoint_path, os.path.basename(ckpt_path)) + if not os.path.isfile(file_checkpoint): + shutil.copy2(ckpt_path, file_checkpoint) + print("copy checkpoint for finetune") + + # Use the tokenizer and tokenizer_path provided in the command line arguments + tokenizer = args.tokenizer + if tokenizer == "custom": + if not args.tokenizer_path: + raise ValueError("Custom tokenizer selected, but no tokenizer_path provided.") + tokenizer_path = args.tokenizer_path + else: + tokenizer_path = args.dataset_name + + vocab_char_map, vocab_size = get_tokenizer(tokenizer_path, tokenizer) + + print("\nvocab : ", vocab_size) + + mel_spec_kwargs = dict( + target_sample_rate=target_sample_rate, + n_mel_channels=n_mel_channels, + hop_length=hop_length, + ) + + model = CFM( + transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels), + mel_spec_kwargs=mel_spec_kwargs, + vocab_char_map=vocab_char_map, + ) + + trainer = Trainer( + model, + args.epochs, + args.learning_rate, + num_warmup_updates=args.num_warmup_updates, + save_per_updates=args.save_per_updates, + checkpoint_path=checkpoint_path, + batch_size=args.batch_size_per_gpu, + batch_size_type=args.batch_size_type, + max_samples=args.max_samples, + grad_accumulation_steps=args.grad_accumulation_steps, + max_grad_norm=args.max_grad_norm, + logger=args.logger, + wandb_project=args.dataset_name, + wandb_run_name=args.exp_name, + wandb_resume_id=wandb_resume_id, + log_samples=args.log_samples, + last_per_steps=args.last_per_steps, + ) + + train_dataset = load_dataset(args.dataset_name, tokenizer, mel_spec_kwargs=mel_spec_kwargs) + + trainer.train( + train_dataset, + resumable_with_seed=666, # seed for shuffling dataset + ) + + +if __name__ == "__main__": + main() diff --git a/src/f5_tts/train/finetune_gradio.py b/src/f5_tts/train/finetune_gradio.py new file mode 100644 index 0000000000000000000000000000000000000000..e79f758fccc5b5d7e23a57e07452501f491a9c04 --- /dev/null +++ b/src/f5_tts/train/finetune_gradio.py @@ -0,0 +1,1821 @@ +import threading +import queue +import re + +import gc +import json +import os +import platform +import psutil +import random +import signal +import shutil +import subprocess +import sys +import tempfile +import time +from glob import glob + +import click +import gradio as gr +import librosa +import numpy as np +import torch +import torchaudio +from datasets import Dataset as Dataset_ +from datasets.arrow_writer import ArrowWriter +from safetensors.torch import save_file +from scipy.io import wavfile +from transformers import pipeline +from cached_path import cached_path +from f5_tts.api import F5TTS +from f5_tts.model.utils import convert_char_to_pinyin +from importlib.resources import files + +training_process = None +system = platform.system() +python_executable = sys.executable or "python" +tts_api = None +last_checkpoint = "" +last_device = "" +last_ema = None + + +path_data = str(files("f5_tts").joinpath("../../data")) +path_project_ckpts = str(files("f5_tts").joinpath("../../ckpts")) +file_train = "src/f5_tts/train/finetune_cli.py" + +device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" + +pipe = None + + +# Save settings from a JSON file +def save_settings( + project_name, + exp_name, + learning_rate, + batch_size_per_gpu, + batch_size_type, + max_samples, + grad_accumulation_steps, + max_grad_norm, + epochs, + num_warmup_updates, + save_per_updates, + last_per_steps, + finetune, + file_checkpoint_train, + tokenizer_type, + tokenizer_file, + mixed_precision, + logger, +): + path_project = os.path.join(path_project_ckpts, project_name) + os.makedirs(path_project, exist_ok=True) + file_setting = os.path.join(path_project, "setting.json") + + settings = { + "exp_name": exp_name, + "learning_rate": learning_rate, + "batch_size_per_gpu": batch_size_per_gpu, + "batch_size_type": batch_size_type, + "max_samples": max_samples, + "grad_accumulation_steps": grad_accumulation_steps, + "max_grad_norm": max_grad_norm, + "epochs": epochs, + "num_warmup_updates": num_warmup_updates, + "save_per_updates": save_per_updates, + "last_per_steps": last_per_steps, + "finetune": finetune, + "file_checkpoint_train": file_checkpoint_train, + "tokenizer_type": tokenizer_type, + "tokenizer_file": tokenizer_file, + "mixed_precision": mixed_precision, + "logger": logger, + } + with open(file_setting, "w") as f: + json.dump(settings, f, indent=4) + return "Settings saved!" + + +# Load settings from a JSON file +def load_settings(project_name): + project_name = project_name.replace("_pinyin", "").replace("_char", "") + path_project = os.path.join(path_project_ckpts, project_name) + file_setting = os.path.join(path_project, "setting.json") + + if not os.path.isfile(file_setting): + settings = { + "exp_name": "F5TTS_Base", + "learning_rate": 1e-05, + "batch_size_per_gpu": 1000, + "batch_size_type": "frame", + "max_samples": 64, + "grad_accumulation_steps": 1, + "max_grad_norm": 1, + "epochs": 100, + "num_warmup_updates": 2, + "save_per_updates": 300, + "last_per_steps": 100, + "finetune": True, + "file_checkpoint_train": "", + "tokenizer_type": "pinyin", + "tokenizer_file": "", + "mixed_precision": "none", + "logger": "wandb", + } + return ( + settings["exp_name"], + settings["learning_rate"], + settings["batch_size_per_gpu"], + settings["batch_size_type"], + settings["max_samples"], + settings["grad_accumulation_steps"], + settings["max_grad_norm"], + settings["epochs"], + settings["num_warmup_updates"], + settings["save_per_updates"], + settings["last_per_steps"], + settings["finetune"], + settings["file_checkpoint_train"], + settings["tokenizer_type"], + settings["tokenizer_file"], + settings["mixed_precision"], + settings["logger"], + ) + + with open(file_setting, "r") as f: + settings = json.load(f) + if "logger" not in settings: + settings["logger"] = "wandb" + return ( + settings["exp_name"], + settings["learning_rate"], + settings["batch_size_per_gpu"], + settings["batch_size_type"], + settings["max_samples"], + settings["grad_accumulation_steps"], + settings["max_grad_norm"], + settings["epochs"], + settings["num_warmup_updates"], + settings["save_per_updates"], + settings["last_per_steps"], + settings["finetune"], + settings["file_checkpoint_train"], + settings["tokenizer_type"], + settings["tokenizer_file"], + settings["mixed_precision"], + settings["logger"], + ) + + +# Load metadata +def get_audio_duration(audio_path): + """Calculate the duration mono of an audio file.""" + audio, sample_rate = torchaudio.load(audio_path) + return audio.shape[1] / sample_rate + + +def clear_text(text): + """Clean and prepare text by lowering the case and stripping whitespace.""" + return text.lower().strip() + + +def get_rms( + y, + frame_length=2048, + hop_length=512, + pad_mode="constant", +): # https://github.com/RVC-Boss/GPT-SoVITS/blob/main/tools/slicer2.py + padding = (int(frame_length // 2), int(frame_length // 2)) + y = np.pad(y, padding, mode=pad_mode) + + axis = -1 + # put our new within-frame axis at the end for now + out_strides = y.strides + tuple([y.strides[axis]]) + # Reduce the shape on the framing axis + x_shape_trimmed = list(y.shape) + x_shape_trimmed[axis] -= frame_length - 1 + out_shape = tuple(x_shape_trimmed) + tuple([frame_length]) + xw = np.lib.stride_tricks.as_strided(y, shape=out_shape, strides=out_strides) + if axis < 0: + target_axis = axis - 1 + else: + target_axis = axis + 1 + xw = np.moveaxis(xw, -1, target_axis) + # Downsample along the target axis + slices = [slice(None)] * xw.ndim + slices[axis] = slice(0, None, hop_length) + x = xw[tuple(slices)] + + # Calculate power + power = np.mean(np.abs(x) ** 2, axis=-2, keepdims=True) + + return np.sqrt(power) + + +class Slicer: # https://github.com/RVC-Boss/GPT-SoVITS/blob/main/tools/slicer2.py + def __init__( + self, + sr: int, + threshold: float = -40.0, + min_length: int = 2000, + min_interval: int = 300, + hop_size: int = 20, + max_sil_kept: int = 2000, + ): + if not min_length >= min_interval >= hop_size: + raise ValueError("The following condition must be satisfied: min_length >= min_interval >= hop_size") + if not max_sil_kept >= hop_size: + raise ValueError("The following condition must be satisfied: max_sil_kept >= hop_size") + min_interval = sr * min_interval / 1000 + self.threshold = 10 ** (threshold / 20.0) + self.hop_size = round(sr * hop_size / 1000) + self.win_size = min(round(min_interval), 4 * self.hop_size) + self.min_length = round(sr * min_length / 1000 / self.hop_size) + self.min_interval = round(min_interval / self.hop_size) + self.max_sil_kept = round(sr * max_sil_kept / 1000 / self.hop_size) + + def _apply_slice(self, waveform, begin, end): + if len(waveform.shape) > 1: + return waveform[:, begin * self.hop_size : min(waveform.shape[1], end * self.hop_size)] + else: + return waveform[begin * self.hop_size : min(waveform.shape[0], end * self.hop_size)] + + # @timeit + def slice(self, waveform): + if len(waveform.shape) > 1: + samples = waveform.mean(axis=0) + else: + samples = waveform + if samples.shape[0] <= self.min_length: + return [waveform] + rms_list = get_rms(y=samples, frame_length=self.win_size, hop_length=self.hop_size).squeeze(0) + sil_tags = [] + silence_start = None + clip_start = 0 + for i, rms in enumerate(rms_list): + # Keep looping while frame is silent. + if rms < self.threshold: + # Record start of silent frames. + if silence_start is None: + silence_start = i + continue + # Keep looping while frame is not silent and silence start has not been recorded. + if silence_start is None: + continue + # Clear recorded silence start if interval is not enough or clip is too short + is_leading_silence = silence_start == 0 and i > self.max_sil_kept + need_slice_middle = i - silence_start >= self.min_interval and i - clip_start >= self.min_length + if not is_leading_silence and not need_slice_middle: + silence_start = None + continue + # Need slicing. Record the range of silent frames to be removed. + if i - silence_start <= self.max_sil_kept: + pos = rms_list[silence_start : i + 1].argmin() + silence_start + if silence_start == 0: + sil_tags.append((0, pos)) + else: + sil_tags.append((pos, pos)) + clip_start = pos + elif i - silence_start <= self.max_sil_kept * 2: + pos = rms_list[i - self.max_sil_kept : silence_start + self.max_sil_kept + 1].argmin() + pos += i - self.max_sil_kept + pos_l = rms_list[silence_start : silence_start + self.max_sil_kept + 1].argmin() + silence_start + pos_r = rms_list[i - self.max_sil_kept : i + 1].argmin() + i - self.max_sil_kept + if silence_start == 0: + sil_tags.append((0, pos_r)) + clip_start = pos_r + else: + sil_tags.append((min(pos_l, pos), max(pos_r, pos))) + clip_start = max(pos_r, pos) + else: + pos_l = rms_list[silence_start : silence_start + self.max_sil_kept + 1].argmin() + silence_start + pos_r = rms_list[i - self.max_sil_kept : i + 1].argmin() + i - self.max_sil_kept + if silence_start == 0: + sil_tags.append((0, pos_r)) + else: + sil_tags.append((pos_l, pos_r)) + clip_start = pos_r + silence_start = None + # Deal with trailing silence. + total_frames = rms_list.shape[0] + if silence_start is not None and total_frames - silence_start >= self.min_interval: + silence_end = min(total_frames, silence_start + self.max_sil_kept) + pos = rms_list[silence_start : silence_end + 1].argmin() + silence_start + sil_tags.append((pos, total_frames + 1)) + # Apply and return slices. + ####音频+起始时间+终止时间 + if len(sil_tags) == 0: + return [[waveform, 0, int(total_frames * self.hop_size)]] + else: + chunks = [] + if sil_tags[0][0] > 0: + chunks.append([self._apply_slice(waveform, 0, sil_tags[0][0]), 0, int(sil_tags[0][0] * self.hop_size)]) + for i in range(len(sil_tags) - 1): + chunks.append( + [ + self._apply_slice(waveform, sil_tags[i][1], sil_tags[i + 1][0]), + int(sil_tags[i][1] * self.hop_size), + int(sil_tags[i + 1][0] * self.hop_size), + ] + ) + if sil_tags[-1][1] < total_frames: + chunks.append( + [ + self._apply_slice(waveform, sil_tags[-1][1], total_frames), + int(sil_tags[-1][1] * self.hop_size), + int(total_frames * self.hop_size), + ] + ) + return chunks + + +# terminal +def terminate_process_tree(pid, including_parent=True): + try: + parent = psutil.Process(pid) + except psutil.NoSuchProcess: + # Process already terminated + return + + children = parent.children(recursive=True) + for child in children: + try: + os.kill(child.pid, signal.SIGTERM) # or signal.SIGKILL + except OSError: + pass + if including_parent: + try: + os.kill(parent.pid, signal.SIGTERM) # or signal.SIGKILL + except OSError: + pass + + +def terminate_process(pid): + if system == "Windows": + cmd = f"taskkill /t /f /pid {pid}" + os.system(cmd) + else: + terminate_process_tree(pid) + + +def start_training( + dataset_name="", + exp_name="F5TTS_Base", + learning_rate=1e-4, + batch_size_per_gpu=400, + batch_size_type="frame", + max_samples=64, + grad_accumulation_steps=1, + max_grad_norm=1.0, + epochs=11, + num_warmup_updates=200, + save_per_updates=400, + last_per_steps=800, + finetune=True, + file_checkpoint_train="", + tokenizer_type="pinyin", + tokenizer_file="", + mixed_precision="fp16", + stream=False, + logger="wandb", +): + global training_process, tts_api, stop_signal, pipe + + if tts_api is not None or pipe is not None: + if tts_api is not None: + del tts_api + if pipe is not None: + del pipe + gc.collect() + torch.cuda.empty_cache() + tts_api = None + pipe = None + + path_project = os.path.join(path_data, dataset_name) + + if not os.path.isdir(path_project): + yield ( + f"There is not project with name {dataset_name}", + gr.update(interactive=True), + gr.update(interactive=False), + ) + return + + file_raw = os.path.join(path_project, "raw.arrow") + if not os.path.isfile(file_raw): + yield f"There is no file {file_raw}", gr.update(interactive=True), gr.update(interactive=False) + return + + # Check if a training process is already running + if training_process is not None: + return "Train run already!", gr.update(interactive=False), gr.update(interactive=True) + + yield "start train", gr.update(interactive=False), gr.update(interactive=False) + + # Command to run the training script with the specified arguments + + if tokenizer_file == "": + if dataset_name.endswith("_pinyin"): + tokenizer_type = "pinyin" + elif dataset_name.endswith("_char"): + tokenizer_type = "char" + else: + tokenizer_type = "custom" + + dataset_name = dataset_name.replace("_pinyin", "").replace("_char", "") + + if mixed_precision != "none": + fp16 = f"--mixed_precision={mixed_precision}" + else: + fp16 = "" + + cmd = ( + f"accelerate launch {fp16} {file_train} --exp_name {exp_name} " + f"--learning_rate {learning_rate} " + f"--batch_size_per_gpu {batch_size_per_gpu} " + f"--batch_size_type {batch_size_type} " + f"--max_samples {max_samples} " + f"--grad_accumulation_steps {grad_accumulation_steps} " + f"--max_grad_norm {max_grad_norm} " + f"--epochs {epochs} " + f"--num_warmup_updates {num_warmup_updates} " + f"--save_per_updates {save_per_updates} " + f"--last_per_steps {last_per_steps} " + f"--dataset_name {dataset_name}" + ) + + if finetune: + cmd += f" --finetune {finetune}" + + if file_checkpoint_train != "": + cmd += f" --file_checkpoint_train {file_checkpoint_train}" + + if tokenizer_file != "": + cmd += f" --tokenizer_path {tokenizer_file}" + + cmd += f" --tokenizer {tokenizer_type} " + + cmd += f" --log_samples True --logger {logger} " + + print(cmd) + + save_settings( + dataset_name, + exp_name, + learning_rate, + batch_size_per_gpu, + batch_size_type, + max_samples, + grad_accumulation_steps, + max_grad_norm, + epochs, + num_warmup_updates, + save_per_updates, + last_per_steps, + finetune, + file_checkpoint_train, + tokenizer_type, + tokenizer_file, + mixed_precision, + logger, + ) + + try: + if not stream: + # Start the training process + training_process = subprocess.Popen(cmd, shell=True) + + time.sleep(5) + yield "train start", gr.update(interactive=False), gr.update(interactive=True) + + # Wait for the training process to finish + training_process.wait() + else: + + def stream_output(pipe, output_queue): + try: + for line in iter(pipe.readline, ""): + output_queue.put(line) + except Exception as e: + output_queue.put(f"Error reading pipe: {str(e)}") + finally: + pipe.close() + + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + + training_process = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, env=env + ) + yield "Training started...", gr.update(interactive=False), gr.update(interactive=True) + + stdout_queue = queue.Queue() + stderr_queue = queue.Queue() + + stdout_thread = threading.Thread(target=stream_output, args=(training_process.stdout, stdout_queue)) + stderr_thread = threading.Thread(target=stream_output, args=(training_process.stderr, stderr_queue)) + stdout_thread.daemon = True + stderr_thread.daemon = True + stdout_thread.start() + stderr_thread.start() + stop_signal = False + while True: + if stop_signal: + training_process.terminate() + time.sleep(0.5) + if training_process.poll() is None: + training_process.kill() + yield "Training stopped by user.", gr.update(interactive=True), gr.update(interactive=False) + break + + process_status = training_process.poll() + + # Handle stdout + try: + while True: + output = stdout_queue.get_nowait() + print(output, end="") + match = re.search( + r"Epoch (\d+)/(\d+):\s+(\d+)%\|.*\[(\d+:\d+)<.*?loss=(\d+\.\d+), step=(\d+)", output + ) + if match: + current_epoch = match.group(1) + total_epochs = match.group(2) + percent_complete = match.group(3) + elapsed_time = match.group(4) + loss = match.group(5) + current_step = match.group(6) + message = ( + f"Epoch: {current_epoch}/{total_epochs}, " + f"Progress: {percent_complete}%, " + f"Elapsed Time: {elapsed_time}, " + f"Loss: {loss}, " + f"Step: {current_step}" + ) + yield message, gr.update(interactive=False), gr.update(interactive=True) + elif output.strip(): + yield output, gr.update(interactive=False), gr.update(interactive=True) + except queue.Empty: + pass + + # Handle stderr + try: + while True: + error_output = stderr_queue.get_nowait() + print(error_output, end="") + if error_output.strip(): + yield f"{error_output.strip()}", gr.update(interactive=False), gr.update(interactive=True) + except queue.Empty: + pass + + if process_status is not None and stdout_queue.empty() and stderr_queue.empty(): + if process_status != 0: + yield ( + f"Process crashed with exit code {process_status}!", + gr.update(interactive=False), + gr.update(interactive=True), + ) + else: + yield "Training complete!", gr.update(interactive=False), gr.update(interactive=True) + break + + # Small sleep to prevent CPU thrashing + time.sleep(0.1) + + # Clean up + training_process.stdout.close() + training_process.stderr.close() + training_process.wait() + + time.sleep(1) + + if training_process is None: + text_info = "train stop" + else: + text_info = "train complete !" + + except Exception as e: # Catch all exceptions + # Ensure that we reset the training process variable in case of an error + text_info = f"An error occurred: {str(e)}" + + training_process = None + + yield text_info, gr.update(interactive=True), gr.update(interactive=False) + + +def stop_training(): + global training_process, stop_signal + + if training_process is None: + return "Train not run !", gr.update(interactive=True), gr.update(interactive=False) + terminate_process_tree(training_process.pid) + # training_process = None + stop_signal = True + return "train stop", gr.update(interactive=True), gr.update(interactive=False) + + +def get_list_projects(): + project_list = [] + for folder in os.listdir(path_data): + path_folder = os.path.join(path_data, folder) + if not os.path.isdir(path_folder): + continue + folder = folder.lower() + if folder == "emilia_zh_en_pinyin": + continue + project_list.append(folder) + + projects_selelect = None if not project_list else project_list[-1] + + return project_list, projects_selelect + + +def create_data_project(name, tokenizer_type): + name += "_" + tokenizer_type + os.makedirs(os.path.join(path_data, name), exist_ok=True) + os.makedirs(os.path.join(path_data, name, "dataset"), exist_ok=True) + project_list, projects_selelect = get_list_projects() + return gr.update(choices=project_list, value=name) + + +def transcribe(file_audio, language="english"): + global pipe + + if pipe is None: + pipe = pipeline( + "automatic-speech-recognition", + model="openai/whisper-large-v3-turbo", + torch_dtype=torch.float16, + device=device, + ) + + text_transcribe = pipe( + file_audio, + chunk_length_s=30, + batch_size=128, + generate_kwargs={"task": "transcribe", "language": language}, + return_timestamps=False, + )["text"].strip() + return text_transcribe + + +def transcribe_all(name_project, audio_files, language, user=False, progress=gr.Progress()): + path_project = os.path.join(path_data, name_project) + path_dataset = os.path.join(path_project, "dataset") + path_project_wavs = os.path.join(path_project, "wavs") + file_metadata = os.path.join(path_project, "metadata.csv") + + if not user: + if audio_files is None: + return "You need to load an audio file." + + if os.path.isdir(path_project_wavs): + shutil.rmtree(path_project_wavs) + + if os.path.isfile(file_metadata): + os.remove(file_metadata) + + os.makedirs(path_project_wavs, exist_ok=True) + + if user: + file_audios = [ + file + for format in ("*.wav", "*.ogg", "*.opus", "*.mp3", "*.flac") + for file in glob(os.path.join(path_dataset, format)) + ] + if file_audios == []: + return "No audio file was found in the dataset." + else: + file_audios = audio_files + + alpha = 0.5 + _max = 1.0 + slicer = Slicer(24000) + + num = 0 + error_num = 0 + data = "" + for file_audio in progress.tqdm(file_audios, desc="transcribe files", total=len((file_audios))): + audio, _ = librosa.load(file_audio, sr=24000, mono=True) + + list_slicer = slicer.slice(audio) + for chunk, start, end in progress.tqdm(list_slicer, total=len(list_slicer), desc="slicer files"): + name_segment = os.path.join(f"segment_{num}") + file_segment = os.path.join(path_project_wavs, f"{name_segment}.wav") + + tmp_max = np.abs(chunk).max() + if tmp_max > 1: + chunk /= tmp_max + chunk = (chunk / tmp_max * (_max * alpha)) + (1 - alpha) * chunk + wavfile.write(file_segment, 24000, (chunk * 32767).astype(np.int16)) + + try: + text = transcribe(file_segment, language) + text = text.lower().strip().replace('"', "") + + data += f"{name_segment}|{text}\n" + + num += 1 + except: # noqa: E722 + error_num += 1 + + with open(file_metadata, "w", encoding="utf-8-sig") as f: + f.write(data) + + if error_num != []: + error_text = f"\nerror files : {error_num}" + else: + error_text = "" + + return f"transcribe complete samples : {num}\npath : {path_project_wavs}{error_text}" + + +def format_seconds_to_hms(seconds): + hours = int(seconds / 3600) + minutes = int((seconds % 3600) / 60) + seconds = seconds % 60 + return "{:02d}:{:02d}:{:02d}".format(hours, minutes, int(seconds)) + + +def get_correct_audio_path( + audio_input, + base_path="wavs", + supported_formats=("wav", "mp3", "aac", "flac", "m4a", "alac", "ogg", "aiff", "wma", "amr"), +): + file_audio = None + + # Helper function to check if file has a supported extension + def has_supported_extension(file_name): + return any(file_name.endswith(f".{ext}") for ext in supported_formats) + + # Case 1: If it's a full path with a valid extension, use it directly + if os.path.isabs(audio_input) and has_supported_extension(audio_input): + file_audio = audio_input + + # Case 2: If it has a supported extension but is not a full path + elif has_supported_extension(audio_input) and not os.path.isabs(audio_input): + file_audio = os.path.join(base_path, audio_input) + print("2") + + # Case 3: If only the name is given (no extension and not a full path) + elif not has_supported_extension(audio_input) and not os.path.isabs(audio_input): + print("3") + for ext in supported_formats: + potential_file = os.path.join(base_path, f"{audio_input}.{ext}") + if os.path.exists(potential_file): + file_audio = potential_file + break + else: + file_audio = os.path.join(base_path, f"{audio_input}.{supported_formats[0]}") + return file_audio + + +def create_metadata(name_project, ch_tokenizer, progress=gr.Progress()): + path_project = os.path.join(path_data, name_project) + path_project_wavs = os.path.join(path_project, "wavs") + file_metadata = os.path.join(path_project, "metadata.csv") + file_raw = os.path.join(path_project, "raw.arrow") + file_duration = os.path.join(path_project, "duration.json") + file_vocab = os.path.join(path_project, "vocab.txt") + + if not os.path.isfile(file_metadata): + return "The file was not found in " + file_metadata, "" + + with open(file_metadata, "r", encoding="utf-8-sig") as f: + data = f.read() + + audio_path_list = [] + text_list = [] + duration_list = [] + + count = data.split("\n") + lenght = 0 + result = [] + error_files = [] + text_vocab_set = set() + for line in progress.tqdm(data.split("\n"), total=count): + sp_line = line.split("|") + if len(sp_line) != 2: + continue + name_audio, text = sp_line[:2] + + file_audio = get_correct_audio_path(name_audio, path_project_wavs) + + if not os.path.isfile(file_audio): + error_files.append([file_audio, "error path"]) + continue + + try: + duration = get_audio_duration(file_audio) + except Exception as e: + error_files.append([file_audio, "duration"]) + print(f"Error processing {file_audio}: {e}") + continue + + if duration < 1 or duration > 25: + error_files.append([file_audio, "duration < 1 or > 25 "]) + continue + if len(text) < 4: + error_files.append([file_audio, "very small text len 3"]) + continue + + text = clear_text(text) + text = convert_char_to_pinyin([text], polyphone=True)[0] + + audio_path_list.append(file_audio) + duration_list.append(duration) + text_list.append(text) + + result.append({"audio_path": file_audio, "text": text, "duration": duration}) + if ch_tokenizer: + text_vocab_set.update(list(text)) + + lenght += duration + + if duration_list == []: + return f"Error: No audio files found in the specified path : {path_project_wavs}", "" + + min_second = round(min(duration_list), 2) + max_second = round(max(duration_list), 2) + + with ArrowWriter(path=file_raw, writer_batch_size=1) as writer: + for line in progress.tqdm(result, total=len(result), desc="prepare data"): + writer.write(line) + + with open(file_duration, "w") as f: + json.dump({"duration": duration_list}, f, ensure_ascii=False) + + new_vocal = "" + if not ch_tokenizer: + if not os.path.isfile(file_vocab): + file_vocab_finetune = os.path.join(path_data, "Emilia_ZH_EN_pinyin/vocab.txt") + if not os.path.isfile(file_vocab_finetune): + return "Error: Vocabulary file 'Emilia_ZH_EN_pinyin' not found!", "" + shutil.copy2(file_vocab_finetune, file_vocab) + + with open(file_vocab, "r", encoding="utf-8-sig") as f: + vocab_char_map = {} + for i, char in enumerate(f): + vocab_char_map[char[:-1]] = i + vocab_size = len(vocab_char_map) + + else: + with open(file_vocab, "w", encoding="utf-8-sig") as f: + for vocab in sorted(text_vocab_set): + f.write(vocab + "\n") + new_vocal += vocab + "\n" + vocab_size = len(text_vocab_set) + + if error_files != []: + error_text = "\n".join([" = ".join(item) for item in error_files]) + else: + error_text = "" + + return ( + f"prepare complete \nsamples : {len(text_list)}\ntime data : {format_seconds_to_hms(lenght)}\nmin sec : {min_second}\nmax sec : {max_second}\nfile_arrow : {file_raw}\nvocab : {vocab_size}\n{error_text}", + new_vocal, + ) + + +def check_user(value): + return gr.update(visible=not value), gr.update(visible=value) + + +def calculate_train( + name_project, + batch_size_type, + max_samples, + learning_rate, + num_warmup_updates, + save_per_updates, + last_per_steps, + finetune, +): + path_project = os.path.join(path_data, name_project) + file_duraction = os.path.join(path_project, "duration.json") + + if not os.path.isfile(file_duraction): + return ( + 1000, + max_samples, + num_warmup_updates, + save_per_updates, + last_per_steps, + "project not found !", + learning_rate, + ) + + with open(file_duraction, "r") as file: + data = json.load(file) + + duration_list = data["duration"] + samples = len(duration_list) + hours = sum(duration_list) / 3600 + + # if torch.cuda.is_available(): + # gpu_properties = torch.cuda.get_device_properties(0) + # total_memory = gpu_properties.total_memory / (1024**3) + # elif torch.backends.mps.is_available(): + # total_memory = psutil.virtual_memory().available / (1024**3) + + if torch.cuda.is_available(): + gpu_count = torch.cuda.device_count() + total_memory = 0 + for i in range(gpu_count): + gpu_properties = torch.cuda.get_device_properties(i) + total_memory += gpu_properties.total_memory / (1024**3) # in GB + + elif torch.backends.mps.is_available(): + gpu_count = 1 + total_memory = psutil.virtual_memory().available / (1024**3) + + if batch_size_type == "frame": + batch = int(total_memory * 0.5) + batch = (lambda num: num + 1 if num % 2 != 0 else num)(batch) + batch_size_per_gpu = int(38400 / batch) + else: + batch_size_per_gpu = int(total_memory / 8) + batch_size_per_gpu = (lambda num: num + 1 if num % 2 != 0 else num)(batch_size_per_gpu) + batch = batch_size_per_gpu + + if batch_size_per_gpu <= 0: + batch_size_per_gpu = 1 + + if samples < 64: + max_samples = int(samples * 0.25) + else: + max_samples = 64 + + num_warmup_updates = int(samples * 0.05) + save_per_updates = int(samples * 0.10) + last_per_steps = int(save_per_updates * 0.25) + + max_samples = (lambda num: num + 1 if num % 2 != 0 else num)(max_samples) + num_warmup_updates = (lambda num: num + 1 if num % 2 != 0 else num)(num_warmup_updates) + save_per_updates = (lambda num: num + 1 if num % 2 != 0 else num)(save_per_updates) + last_per_steps = (lambda num: num + 1 if num % 2 != 0 else num)(last_per_steps) + if last_per_steps <= 0: + last_per_steps = 2 + + total_hours = hours + mel_hop_length = 256 + mel_sampling_rate = 24000 + + # target + wanted_max_updates = 1000000 + + # train params + gpus = gpu_count + frames_per_gpu = batch_size_per_gpu # 8 * 38400 = 307200 + grad_accum = 1 + + # intermediate + mini_batch_frames = frames_per_gpu * grad_accum * gpus + mini_batch_hours = mini_batch_frames * mel_hop_length / mel_sampling_rate / 3600 + updates_per_epoch = total_hours / mini_batch_hours + # steps_per_epoch = updates_per_epoch * grad_accum + epochs = wanted_max_updates / updates_per_epoch + + if finetune: + learning_rate = 1e-5 + else: + learning_rate = 7.5e-5 + + return ( + batch_size_per_gpu, + max_samples, + num_warmup_updates, + save_per_updates, + last_per_steps, + samples, + learning_rate, + int(epochs), + ) + + +def extract_and_save_ema_model(checkpoint_path: str, new_checkpoint_path: str, safetensors: bool) -> str: + try: + checkpoint = torch.load(checkpoint_path) + print("Original Checkpoint Keys:", checkpoint.keys()) + + ema_model_state_dict = checkpoint.get("ema_model_state_dict", None) + if ema_model_state_dict is None: + return "No 'ema_model_state_dict' found in the checkpoint." + + if safetensors: + new_checkpoint_path = new_checkpoint_path.replace(".pt", ".safetensors") + save_file(ema_model_state_dict, new_checkpoint_path) + else: + new_checkpoint_path = new_checkpoint_path.replace(".safetensors", ".pt") + new_checkpoint = {"ema_model_state_dict": ema_model_state_dict} + torch.save(new_checkpoint, new_checkpoint_path) + + return f"New checkpoint saved at: {new_checkpoint_path}" + + except Exception as e: + return f"An error occurred: {e}" + + +def expand_model_embeddings(ckpt_path, new_ckpt_path, num_new_tokens=42): + seed = 666 + random.seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + ckpt = torch.load(ckpt_path, map_location="cpu") + + ema_sd = ckpt.get("ema_model_state_dict", {}) + embed_key_ema = "ema_model.transformer.text_embed.text_embed.weight" + old_embed_ema = ema_sd[embed_key_ema] + + vocab_old = old_embed_ema.size(0) + embed_dim = old_embed_ema.size(1) + vocab_new = vocab_old + num_new_tokens + + def expand_embeddings(old_embeddings): + new_embeddings = torch.zeros((vocab_new, embed_dim)) + new_embeddings[:vocab_old] = old_embeddings + new_embeddings[vocab_old:] = torch.randn((num_new_tokens, embed_dim)) + return new_embeddings + + ema_sd[embed_key_ema] = expand_embeddings(ema_sd[embed_key_ema]) + + torch.save(ckpt, new_ckpt_path) + + return vocab_new + + +def vocab_count(text): + return str(len(text.split(","))) + + +def vocab_extend(project_name, symbols, model_type): + if symbols == "": + return "Symbols empty!" + + name_project = project_name + path_project = os.path.join(path_data, name_project) + file_vocab_project = os.path.join(path_project, "vocab.txt") + + file_vocab = os.path.join(path_data, "Emilia_ZH_EN_pinyin/vocab.txt") + if not os.path.isfile(file_vocab): + return f"the file {file_vocab} not found !" + + symbols = symbols.split(",") + if symbols == []: + return "Symbols to extend not found." + + with open(file_vocab, "r", encoding="utf-8-sig") as f: + data = f.read() + vocab = data.split("\n") + vocab_check = set(vocab) + + miss_symbols = [] + for item in symbols: + item = item.replace(" ", "") + if item in vocab_check: + continue + miss_symbols.append(item) + + if miss_symbols == []: + return "Symbols are okay no need to extend." + + size_vocab = len(vocab) + vocab.pop() + for item in miss_symbols: + vocab.append(item) + + vocab.append("") + + with open(file_vocab_project, "w", encoding="utf-8") as f: + f.write("\n".join(vocab)) + + if model_type == "F5-TTS": + ckpt_path = str(cached_path("hf://SWivid/F5-TTS/F5TTS_Base/model_1200000.pt")) + else: + ckpt_path = str(cached_path("hf://SWivid/E2-TTS/E2TTS_Base/model_1200000.pt")) + + vocab_size_new = len(miss_symbols) + + dataset_name = name_project.replace("_pinyin", "").replace("_char", "") + new_ckpt_path = os.path.join(path_project_ckpts, dataset_name) + os.makedirs(new_ckpt_path, exist_ok=True) + new_ckpt_file = os.path.join(new_ckpt_path, "model_1200000.pt") + + size = expand_model_embeddings(ckpt_path, new_ckpt_file, num_new_tokens=vocab_size_new) + + vocab_new = "\n".join(miss_symbols) + return f"vocab old size : {size_vocab}\nvocab new size : {size}\nvocab add : {vocab_size_new}\nnew symbols :\n{vocab_new}" + + +def vocab_check(project_name): + name_project = project_name + path_project = os.path.join(path_data, name_project) + + file_metadata = os.path.join(path_project, "metadata.csv") + + file_vocab = os.path.join(path_data, "Emilia_ZH_EN_pinyin/vocab.txt") + if not os.path.isfile(file_vocab): + return f"the file {file_vocab} not found !", "" + + with open(file_vocab, "r", encoding="utf-8-sig") as f: + data = f.read() + vocab = data.split("\n") + vocab = set(vocab) + + if not os.path.isfile(file_metadata): + return f"the file {file_metadata} not found !", "" + + with open(file_metadata, "r", encoding="utf-8-sig") as f: + data = f.read() + + miss_symbols = [] + miss_symbols_keep = {} + for item in data.split("\n"): + sp = item.split("|") + if len(sp) != 2: + continue + + text = sp[1].lower().strip() + + for t in text: + if t not in vocab and t not in miss_symbols_keep: + miss_symbols.append(t) + miss_symbols_keep[t] = t + + if miss_symbols == []: + vocab_miss = "" + info = "You can train using your language !" + else: + vocab_miss = ",".join(miss_symbols) + info = f"The following symbols are missing in your language {len(miss_symbols)}\n\n" + + return info, vocab_miss + + +def get_random_sample_prepare(project_name): + name_project = project_name + path_project = os.path.join(path_data, name_project) + file_arrow = os.path.join(path_project, "raw.arrow") + if not os.path.isfile(file_arrow): + return "", None + dataset = Dataset_.from_file(file_arrow) + random_sample = dataset.shuffle(seed=random.randint(0, 1000)).select([0]) + text = "[" + " , ".join(["' " + t + " '" for t in random_sample["text"][0]]) + "]" + audio_path = random_sample["audio_path"][0] + return text, audio_path + + +def get_random_sample_transcribe(project_name): + name_project = project_name + path_project = os.path.join(path_data, name_project) + file_metadata = os.path.join(path_project, "metadata.csv") + if not os.path.isfile(file_metadata): + return "", None + + data = "" + with open(file_metadata, "r", encoding="utf-8-sig") as f: + data = f.read() + + list_data = [] + for item in data.split("\n"): + sp = item.split("|") + if len(sp) != 2: + continue + list_data.append([os.path.join(path_project, "wavs", sp[0] + ".wav"), sp[1]]) + + if list_data == []: + return "", None + + random_item = random.choice(list_data) + + return random_item[1], random_item[0] + + +def get_random_sample_infer(project_name): + text, audio = get_random_sample_transcribe(project_name) + return ( + text, + text, + audio, + ) + + +def infer(project, file_checkpoint, exp_name, ref_text, ref_audio, gen_text, nfe_step, use_ema): + global last_checkpoint, last_device, tts_api, last_ema + + if not os.path.isfile(file_checkpoint): + return None, "checkpoint not found!" + + if training_process is not None: + device_test = "cpu" + else: + device_test = None + + if last_checkpoint != file_checkpoint or last_device != device_test or last_ema != use_ema or tts_api is None: + if last_checkpoint != file_checkpoint: + last_checkpoint = file_checkpoint + + if last_device != device_test: + last_device = device_test + + if last_ema != use_ema: + last_ema = use_ema + + vocab_file = os.path.join(path_data, project, "vocab.txt") + + tts_api = F5TTS( + model_type=exp_name, ckpt_file=file_checkpoint, vocab_file=vocab_file, device=device_test, use_ema=use_ema + ) + + print("update >> ", device_test, file_checkpoint, use_ema) + + with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f: + tts_api.infer(gen_text=gen_text, ref_text=ref_text, ref_file=ref_audio, nfe_step=nfe_step, file_wave=f.name) + return f.name, tts_api.device + + +def check_finetune(finetune): + return gr.update(interactive=finetune), gr.update(interactive=finetune), gr.update(interactive=finetune) + + +def get_checkpoints_project(project_name, is_gradio=True): + if project_name is None: + return [], "" + project_name = project_name.replace("_pinyin", "").replace("_char", "") + + if os.path.isdir(path_project_ckpts): + files_checkpoints = glob(os.path.join(path_project_ckpts, project_name, "*.pt")) + files_checkpoints = sorted( + files_checkpoints, + key=lambda x: int(os.path.basename(x).split("_")[1].split(".")[0]) + if os.path.basename(x) != "model_last.pt" + else float("inf"), + ) + else: + files_checkpoints = [] + + selelect_checkpoint = None if not files_checkpoints else files_checkpoints[0] + + if is_gradio: + return gr.update(choices=files_checkpoints, value=selelect_checkpoint) + + return files_checkpoints, selelect_checkpoint + + +def get_audio_project(project_name, is_gradio=True): + if project_name is None: + return [], "" + project_name = project_name.replace("_pinyin", "").replace("_char", "") + + if os.path.isdir(path_project_ckpts): + files_audios = glob(os.path.join(path_project_ckpts, project_name, "samples", "*.wav")) + files_audios = sorted(files_audios, key=lambda x: int(os.path.basename(x).split("_")[1].split(".")[0])) + + files_audios = [item.replace("_gen.wav", "") for item in files_audios if item.endswith("_gen.wav")] + else: + files_audios = [] + + selelect_checkpoint = None if not files_audios else files_audios[0] + + if is_gradio: + return gr.update(choices=files_audios, value=selelect_checkpoint) + + return files_audios, selelect_checkpoint + + +def get_gpu_stats(): + gpu_stats = "" + + if torch.cuda.is_available(): + gpu_count = torch.cuda.device_count() + for i in range(gpu_count): + gpu_name = torch.cuda.get_device_name(i) + gpu_properties = torch.cuda.get_device_properties(i) + total_memory = gpu_properties.total_memory / (1024**3) # in GB + allocated_memory = torch.cuda.memory_allocated(i) / (1024**2) # in MB + reserved_memory = torch.cuda.memory_reserved(i) / (1024**2) # in MB + + gpu_stats += ( + f"GPU {i} Name: {gpu_name}\n" + f"Total GPU memory (GPU {i}): {total_memory:.2f} GB\n" + f"Allocated GPU memory (GPU {i}): {allocated_memory:.2f} MB\n" + f"Reserved GPU memory (GPU {i}): {reserved_memory:.2f} MB\n\n" + ) + + elif torch.backends.mps.is_available(): + gpu_count = 1 + gpu_stats += "MPS GPU\n" + total_memory = psutil.virtual_memory().total / ( + 1024**3 + ) # Total system memory (MPS doesn't have its own memory) + allocated_memory = 0 + reserved_memory = 0 + + gpu_stats += ( + f"Total system memory: {total_memory:.2f} GB\n" + f"Allocated GPU memory (MPS): {allocated_memory:.2f} MB\n" + f"Reserved GPU memory (MPS): {reserved_memory:.2f} MB\n" + ) + + else: + gpu_stats = "No GPU available" + + return gpu_stats + + +def get_cpu_stats(): + cpu_usage = psutil.cpu_percent(interval=1) + memory_info = psutil.virtual_memory() + memory_used = memory_info.used / (1024**2) + memory_total = memory_info.total / (1024**2) + memory_percent = memory_info.percent + + pid = os.getpid() + process = psutil.Process(pid) + nice_value = process.nice() + + cpu_stats = ( + f"CPU Usage: {cpu_usage:.2f}%\n" + f"System Memory: {memory_used:.2f} MB used / {memory_total:.2f} MB total ({memory_percent}% used)\n" + f"Process Priority (Nice value): {nice_value}" + ) + + return cpu_stats + + +def get_combined_stats(): + gpu_stats = get_gpu_stats() + cpu_stats = get_cpu_stats() + combined_stats = f"### GPU Stats\n{gpu_stats}\n\n### CPU Stats\n{cpu_stats}" + return combined_stats + + +def get_audio_select(file_sample): + select_audio_ref = file_sample + select_audio_gen = file_sample + + if file_sample is not None: + select_audio_ref += "_ref.wav" + select_audio_gen += "_gen.wav" + + return select_audio_ref, select_audio_gen + + +with gr.Blocks() as app: + gr.Markdown( + """ +# E2/F5 TTS Automatic Finetune + +This is a local web UI for F5 TTS with advanced batch processing support. This app supports the following TTS models: + +* [F5-TTS](https://arxiv.org/abs/2410.06885) (A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching) +* [E2 TTS](https://arxiv.org/abs/2406.18009) (Embarrassingly Easy Fully Non-Autoregressive Zero-Shot TTS) + +The checkpoints support English and Chinese. + +For tutorial and updates check here (https://github.com/SWivid/F5-TTS/discussions/143) +""" + ) + + with gr.Row(): + projects, projects_selelect = get_list_projects() + tokenizer_type = gr.Radio(label="Tokenizer Type", choices=["pinyin", "char", "custom"], value="pinyin") + project_name = gr.Textbox(label="Project Name", value="my_speak") + bt_create = gr.Button("Create a New Project") + + with gr.Row(): + cm_project = gr.Dropdown( + choices=projects, value=projects_selelect, label="Project", allow_custom_value=True, scale=6 + ) + ch_refresh_project = gr.Button("Refresh", scale=1) + + bt_create.click(fn=create_data_project, inputs=[project_name, tokenizer_type], outputs=[cm_project]) + + with gr.Tabs(): + with gr.TabItem("Transcribe Data"): + gr.Markdown("""```plaintext +Skip this step if you have your dataset, metadata.csv, and a folder wavs with all the audio files. +```""") + + ch_manual = gr.Checkbox(label="Audio from Path", value=False) + + mark_info_transcribe = gr.Markdown( + """```plaintext + Place your 'wavs' folder and 'metadata.csv' file in the '{your_project_name}' directory. + + my_speak/ + │ + └── dataset/ + ├── audio1.wav + └── audio2.wav + ... + ```""", + visible=False, + ) + + audio_speaker = gr.File(label="Voice", type="filepath", file_count="multiple") + txt_lang = gr.Text(label="Language", value="English") + bt_transcribe = bt_create = gr.Button("Transcribe") + txt_info_transcribe = gr.Text(label="Info", value="") + bt_transcribe.click( + fn=transcribe_all, + inputs=[cm_project, audio_speaker, txt_lang, ch_manual], + outputs=[txt_info_transcribe], + ) + ch_manual.change(fn=check_user, inputs=[ch_manual], outputs=[audio_speaker, mark_info_transcribe]) + + random_sample_transcribe = gr.Button("Random Sample") + + with gr.Row(): + random_text_transcribe = gr.Text(label="Text") + random_audio_transcribe = gr.Audio(label="Audio", type="filepath") + + random_sample_transcribe.click( + fn=get_random_sample_transcribe, + inputs=[cm_project], + outputs=[random_text_transcribe, random_audio_transcribe], + ) + + with gr.TabItem("Vocab Check"): + gr.Markdown("""```plaintext +Check the vocabulary for fine-tuning Emilia_ZH_EN to ensure all symbols are included. For fine-tuning a new language. +```""") + + check_button = gr.Button("Check Vocab") + txt_info_check = gr.Text(label="Info", value="") + + gr.Markdown("""```plaintext +Using the extended model, you can finetune to a new language that is missing symbols in the vocab. This creates a new model with a new vocabulary size and saves it in your ckpts/project folder. +```""") + + exp_name_extend = gr.Radio(label="Model", choices=["F5-TTS", "E2-TTS"], value="F5-TTS") + + with gr.Row(): + txt_extend = gr.Textbox( + label="Symbols", + value="", + placeholder="To add new symbols, make sure to use ',' for each symbol", + scale=6, + ) + txt_count_symbol = gr.Textbox(label="New Vocab Size", value="", scale=1) + + extend_button = gr.Button("Extend") + txt_info_extend = gr.Text(label="Info", value="") + + txt_extend.change(vocab_count, inputs=[txt_extend], outputs=[txt_count_symbol]) + check_button.click(fn=vocab_check, inputs=[cm_project], outputs=[txt_info_check, txt_extend]) + extend_button.click( + fn=vocab_extend, inputs=[cm_project, txt_extend, exp_name_extend], outputs=[txt_info_extend] + ) + + with gr.TabItem("Prepare Data"): + gr.Markdown("""```plaintext +Skip this step if you have your dataset, raw.arrow, duration.json, and vocab.txt +```""") + + gr.Markdown( + """```plaintext + Place all your "wavs" folder and your "metadata.csv" file in your project name directory. + + Supported audio formats: "wav", "mp3", "aac", "flac", "m4a", "alac", "ogg", "aiff", "wma", "amr" + + Example wav format: + my_speak/ + │ + ├── wavs/ + │ ├── audio1.wav + │ └── audio2.wav + | ... + │ + └── metadata.csv + + File format metadata.csv: + + audio1|text1 or audio1.wav|text1 or your_path/audio1.wav|text1 + audio2|text1 or audio2.wav|text1 or your_path/audio2.wav|text1 + ... + + ```""" + ) + ch_tokenizern = gr.Checkbox(label="Create Vocabulary", value=False, visible=False) + bt_prepare = bt_create = gr.Button("Prepare") + txt_info_prepare = gr.Text(label="Info", value="") + txt_vocab_prepare = gr.Text(label="Vocab", value="") + + bt_prepare.click( + fn=create_metadata, inputs=[cm_project, ch_tokenizern], outputs=[txt_info_prepare, txt_vocab_prepare] + ) + + random_sample_prepare = gr.Button("Random Sample") + + with gr.Row(): + random_text_prepare = gr.Text(label="Tokenizer") + random_audio_prepare = gr.Audio(label="Audio", type="filepath") + + random_sample_prepare.click( + fn=get_random_sample_prepare, inputs=[cm_project], outputs=[random_text_prepare, random_audio_prepare] + ) + + with gr.TabItem("Train Data"): + gr.Markdown("""```plaintext +The auto-setting is still experimental. Please make sure that the epochs, save per updates, and last per steps are set correctly, or change them manually as needed. +If you encounter a memory error, try reducing the batch size per GPU to a smaller number. +```""") + with gr.Row(): + bt_calculate = bt_create = gr.Button("Auto Settings") + lb_samples = gr.Label(label="Samples") + batch_size_type = gr.Radio(label="Batch Size Type", choices=["frame", "sample"], value="frame") + + with gr.Row(): + ch_finetune = bt_create = gr.Checkbox(label="Finetune", value=True) + tokenizer_file = gr.Textbox(label="Tokenizer File", value="") + file_checkpoint_train = gr.Textbox(label="Path to the Pretrained Checkpoint", value="") + + with gr.Row(): + exp_name = gr.Radio(label="Model", choices=["F5TTS_Base", "E2TTS_Base"], value="F5TTS_Base") + learning_rate = gr.Number(label="Learning Rate", value=1e-5, step=1e-5) + + with gr.Row(): + batch_size_per_gpu = gr.Number(label="Batch Size per GPU", value=1000) + max_samples = gr.Number(label="Max Samples", value=64) + + with gr.Row(): + grad_accumulation_steps = gr.Number(label="Gradient Accumulation Steps", value=1) + max_grad_norm = gr.Number(label="Max Gradient Norm", value=1.0) + + with gr.Row(): + epochs = gr.Number(label="Epochs", value=10) + num_warmup_updates = gr.Number(label="Warmup Updates", value=2) + + with gr.Row(): + save_per_updates = gr.Number(label="Save per Updates", value=300) + last_per_steps = gr.Number(label="Last per Steps", value=100) + + with gr.Row(): + mixed_precision = gr.Radio(label="mixed_precision", choices=["none", "fp16", "bf16"], value="none") + cd_logger = gr.Radio(label="logger", choices=["wandb", "tensorboard"], value="wandb") + start_button = gr.Button("Start Training") + stop_button = gr.Button("Stop Training", interactive=False) + + if projects_selelect is not None: + ( + exp_namev, + learning_ratev, + batch_size_per_gpuv, + batch_size_typev, + max_samplesv, + grad_accumulation_stepsv, + max_grad_normv, + epochsv, + num_warmupv_updatesv, + save_per_updatesv, + last_per_stepsv, + finetunev, + file_checkpoint_trainv, + tokenizer_typev, + tokenizer_filev, + mixed_precisionv, + cd_loggerv, + ) = load_settings(projects_selelect) + exp_name.value = exp_namev + learning_rate.value = learning_ratev + batch_size_per_gpu.value = batch_size_per_gpuv + batch_size_type.value = batch_size_typev + max_samples.value = max_samplesv + grad_accumulation_steps.value = grad_accumulation_stepsv + max_grad_norm.value = max_grad_normv + epochs.value = epochsv + num_warmup_updates.value = num_warmupv_updatesv + save_per_updates.value = save_per_updatesv + last_per_steps.value = last_per_stepsv + ch_finetune.value = finetunev + file_checkpoint_train.value = file_checkpoint_trainv + tokenizer_type.value = tokenizer_typev + tokenizer_file.value = tokenizer_filev + mixed_precision.value = mixed_precisionv + cd_logger.value = cd_loggerv + + ch_stream = gr.Checkbox(label="Stream Output Experiment", value=True) + txt_info_train = gr.Text(label="Info", value="") + + list_audios, select_audio = get_audio_project(projects_selelect, False) + + select_audio_ref = select_audio + select_audio_gen = select_audio + + if select_audio is not None: + select_audio_ref += "_ref.wav" + select_audio_gen += "_gen.wav" + + with gr.Row(): + ch_list_audio = gr.Dropdown( + choices=list_audios, + value=select_audio, + label="Audios", + allow_custom_value=True, + scale=6, + interactive=True, + ) + bt_stream_audio = gr.Button("Refresh", scale=1) + bt_stream_audio.click(fn=get_audio_project, inputs=[cm_project], outputs=[ch_list_audio]) + cm_project.change(fn=get_audio_project, inputs=[cm_project], outputs=[ch_list_audio]) + + with gr.Row(): + audio_ref_stream = gr.Audio(label="Original", type="filepath", value=select_audio_ref) + audio_gen_stream = gr.Audio(label="Generate", type="filepath", value=select_audio_gen) + + ch_list_audio.change( + fn=get_audio_select, + inputs=[ch_list_audio], + outputs=[audio_ref_stream, audio_gen_stream], + ) + + start_button.click( + fn=start_training, + inputs=[ + cm_project, + exp_name, + learning_rate, + batch_size_per_gpu, + batch_size_type, + max_samples, + grad_accumulation_steps, + max_grad_norm, + epochs, + num_warmup_updates, + save_per_updates, + last_per_steps, + ch_finetune, + file_checkpoint_train, + tokenizer_type, + tokenizer_file, + mixed_precision, + ch_stream, + cd_logger, + ], + outputs=[txt_info_train, start_button, stop_button], + ) + stop_button.click(fn=stop_training, outputs=[txt_info_train, start_button, stop_button]) + + bt_calculate.click( + fn=calculate_train, + inputs=[ + cm_project, + batch_size_type, + max_samples, + learning_rate, + num_warmup_updates, + save_per_updates, + last_per_steps, + ch_finetune, + ], + outputs=[ + batch_size_per_gpu, + max_samples, + num_warmup_updates, + save_per_updates, + last_per_steps, + lb_samples, + learning_rate, + epochs, + ], + ) + + ch_finetune.change( + check_finetune, inputs=[ch_finetune], outputs=[file_checkpoint_train, tokenizer_file, tokenizer_type] + ) + + def setup_load_settings(): + output_components = [ + exp_name, + learning_rate, + batch_size_per_gpu, + batch_size_type, + max_samples, + grad_accumulation_steps, + max_grad_norm, + epochs, + num_warmup_updates, + save_per_updates, + last_per_steps, + ch_finetune, + file_checkpoint_train, + tokenizer_type, + tokenizer_file, + mixed_precision, + cd_logger, + ] + + return output_components + + outputs = setup_load_settings() + + cm_project.change( + fn=load_settings, + inputs=[cm_project], + outputs=outputs, + ) + + ch_refresh_project.click( + fn=load_settings, + inputs=[cm_project], + outputs=outputs, + ) + + with gr.TabItem("Test Model"): + gr.Markdown("""```plaintext +SOS: Check the use_ema setting (True or False) for your model to see what works best for you. +```""") + exp_name = gr.Radio(label="Model", choices=["F5-TTS", "E2-TTS"], value="F5-TTS") + list_checkpoints, checkpoint_select = get_checkpoints_project(projects_selelect, False) + + nfe_step = gr.Number(label="NFE Step", value=32) + ch_use_ema = gr.Checkbox(label="Use EMA", value=True) + with gr.Row(): + cm_checkpoint = gr.Dropdown( + choices=list_checkpoints, value=checkpoint_select, label="Checkpoints", allow_custom_value=True + ) + bt_checkpoint_refresh = gr.Button("Refresh") + + random_sample_infer = gr.Button("Random Sample") + + ref_text = gr.Textbox(label="Ref Text") + ref_audio = gr.Audio(label="Audio Ref", type="filepath") + gen_text = gr.Textbox(label="Gen Text") + + random_sample_infer.click( + fn=get_random_sample_infer, inputs=[cm_project], outputs=[ref_text, gen_text, ref_audio] + ) + + with gr.Row(): + txt_info_gpu = gr.Textbox("", label="Device") + check_button_infer = gr.Button("Infer") + + gen_audio = gr.Audio(label="Audio Gen", type="filepath") + + check_button_infer.click( + fn=infer, + inputs=[cm_project, cm_checkpoint, exp_name, ref_text, ref_audio, gen_text, nfe_step, ch_use_ema], + outputs=[gen_audio, txt_info_gpu], + ) + + bt_checkpoint_refresh.click(fn=get_checkpoints_project, inputs=[cm_project], outputs=[cm_checkpoint]) + cm_project.change(fn=get_checkpoints_project, inputs=[cm_project], outputs=[cm_checkpoint]) + + with gr.TabItem("Reduce Checkpoint"): + gr.Markdown("""```plaintext +Reduce the model size from 5GB to 1.3GB. The new checkpoint can be used for inference or fine-tuning afterward, but it cannot be used to continue training. +```""") + txt_path_checkpoint = gr.Text(label="Path to Checkpoint:") + txt_path_checkpoint_small = gr.Text(label="Path to Output:") + ch_safetensors = gr.Checkbox(label="Safetensors", value="") + txt_info_reduse = gr.Text(label="Info", value="") + reduse_button = gr.Button("Reduce") + reduse_button.click( + fn=extract_and_save_ema_model, + inputs=[txt_path_checkpoint, txt_path_checkpoint_small, ch_safetensors], + outputs=[txt_info_reduse], + ) + + with gr.TabItem("System Info"): + output_box = gr.Textbox(label="GPU and CPU Information", lines=20) + + def update_stats(): + return get_combined_stats() + + update_button = gr.Button("Update Stats") + update_button.click(fn=update_stats, outputs=output_box) + + def auto_update(): + yield gr.update(value=update_stats()) + + gr.update(fn=auto_update, inputs=[], outputs=output_box) + + +@click.command() +@click.option("--port", "-p", default=None, type=int, help="Port to run the app on") +@click.option("--host", "-H", default=None, help="Host to run the app on") +@click.option( + "--share", + "-s", + default=False, + is_flag=True, + help="Share the app via Gradio share link", +) +@click.option("--api", "-a", default=True, is_flag=True, help="Allow API access") +def main(port, host, share, api): + global app + print("Starting app...") + app.queue(api_open=api).launch(server_name=host, server_port=port, share=share, show_api=api) + + +if __name__ == "__main__": + main() diff --git a/src/f5_tts/train/train.py b/src/f5_tts/train/train.py new file mode 100644 index 0000000000000000000000000000000000000000..156afdd66c1b2a86f084feff93353ef01ad9a05c --- /dev/null +++ b/src/f5_tts/train/train.py @@ -0,0 +1,103 @@ +# training script. + +from importlib.resources import files + +from f5_tts.model import CFM, DiT, Trainer, UNetT +from f5_tts.model.dataset import load_dataset +from f5_tts.model.utils import get_tokenizer + +# -------------------------- Dataset Settings --------------------------- # + +target_sample_rate = 24000 +n_mel_channels = 100 +hop_length = 256 +win_length = 1024 +n_fft = 1024 +mel_spec_type = "vocos" # 'vocos' or 'bigvgan' + +tokenizer = "pinyin" # 'pinyin', 'char', or 'custom' +tokenizer_path = None # if tokenizer = 'custom', define the path to the tokenizer you want to use (should be vocab.txt) +dataset_name = "Emilia_ZH_EN" + +# -------------------------- Training Settings -------------------------- # + +exp_name = "F5TTS_Base" # F5TTS_Base | E2TTS_Base + +learning_rate = 7.5e-5 + +batch_size_per_gpu = 38400 # 8 GPUs, 8 * 38400 = 307200 +batch_size_type = "frame" # "frame" or "sample" +max_samples = 64 # max sequences per batch if use frame-wise batch_size. we set 32 for small models, 64 for base models +grad_accumulation_steps = 1 # note: updates = steps / grad_accumulation_steps +max_grad_norm = 1.0 + +epochs = 11 # use linear decay, thus epochs control the slope +num_warmup_updates = 20000 # warmup steps +save_per_updates = 50000 # save checkpoint per steps +last_per_steps = 5000 # save last checkpoint per steps + +# model params +if exp_name == "F5TTS_Base": + wandb_resume_id = None + model_cls = DiT + model_cfg = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4) +elif exp_name == "E2TTS_Base": + wandb_resume_id = None + model_cls = UNetT + model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4) + + +# ----------------------------------------------------------------------- # + + +def main(): + if tokenizer == "custom": + tokenizer_path = tokenizer_path + else: + tokenizer_path = dataset_name + vocab_char_map, vocab_size = get_tokenizer(tokenizer_path, tokenizer) + + mel_spec_kwargs = dict( + n_fft=n_fft, + hop_length=hop_length, + win_length=win_length, + n_mel_channels=n_mel_channels, + target_sample_rate=target_sample_rate, + mel_spec_type=mel_spec_type, + ) + + model = CFM( + transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels), + mel_spec_kwargs=mel_spec_kwargs, + vocab_char_map=vocab_char_map, + ) + + trainer = Trainer( + model, + epochs, + learning_rate, + num_warmup_updates=num_warmup_updates, + save_per_updates=save_per_updates, + checkpoint_path=str(files("f5_tts").joinpath(f"../../ckpts/{exp_name}")), + batch_size=batch_size_per_gpu, + batch_size_type=batch_size_type, + max_samples=max_samples, + grad_accumulation_steps=grad_accumulation_steps, + max_grad_norm=max_grad_norm, + wandb_project="CFM-TTS", + wandb_run_name=exp_name, + wandb_resume_id=wandb_resume_id, + last_per_steps=last_per_steps, + log_samples=True, + mel_spec_type=mel_spec_type, + ) + + train_dataset = load_dataset(dataset_name, tokenizer, mel_spec_kwargs=mel_spec_kwargs) + trainer.train( + train_dataset, + resumable_with_seed=666, # seed for shuffling dataset + ) + + +if __name__ == "__main__": + main() diff --git a/src/third_party/BigVGAN/.gitignore b/src/third_party/BigVGAN/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d95205c2340b913be8b1afe2b1a1f34db6a14717 --- /dev/null +++ b/src/third_party/BigVGAN/.gitignore @@ -0,0 +1,146 @@ +# BigVGAN +alias_free_activation/cuda/build/ +exp/ +tmp/ + +# Symlinks for bundled LibriTTS filelists +filelists/LibriTTS/train-clean-100 +filelists/LibriTTS/train-clean-360 +filelists/LibriTTS/train-other-500 +filelists/LibriTTS/dev-clean +filelists/LibriTTS/dev-other +filelists/LibriTTS/test-clean +filelists/LibriTTS/test-other + +# VSCode configs +.vscode/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ \ No newline at end of file diff --git a/src/third_party/BigVGAN/LICENSE b/src/third_party/BigVGAN/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6016317a8d20a1d7c01bdd34451b424bf212e8c7 --- /dev/null +++ b/src/third_party/BigVGAN/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 NVIDIA CORPORATION. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/third_party/BigVGAN/README.md b/src/third_party/BigVGAN/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c200b5536b4b1fb53238b1de6d7c088f51238ffe --- /dev/null +++ b/src/third_party/BigVGAN/README.md @@ -0,0 +1,266 @@ +## BigVGAN: A Universal Neural Vocoder with Large-Scale Training + +#### Sang-gil Lee, Wei Ping, Boris Ginsburg, Bryan Catanzaro, Sungroh Yoon + +[[Paper]](https://arxiv.org/abs/2206.04658) - [[Code]](https://github.com/NVIDIA/BigVGAN) - [[Showcase]](https://bigvgan-demo.github.io/) - [[Project Page]](https://research.nvidia.com/labs/adlr/projects/bigvgan/) - [[Weights]](https://huggingface.co/collections/nvidia/bigvgan-66959df3d97fd7d98d97dc9a) - [[Demo]](https://huggingface.co/spaces/nvidia/BigVGAN) + +[![PWC](https://img.shields.io/endpoint.svg?url=https://paperswithcode.com/badge/bigvgan-a-universal-neural-vocoder-with-large/speech-synthesis-on-libritts)](https://paperswithcode.com/sota/speech-synthesis-on-libritts?p=bigvgan-a-universal-neural-vocoder-with-large) + +
+ +## News +- **Sep 2024 (v2.4):** + - We have updated the pretrained checkpoints trained for 5M steps. This is final release of the BigVGAN-v2 checkpoints. + +- **Jul 2024 (v2.3):** + - General refactor and code improvements for improved readability. + - Fully fused CUDA kernel of anti-alised activation (upsampling + activation + downsampling) with inference speed benchmark. + +- **Jul 2024 (v2.2):** The repository now includes an interactive local demo using gradio. + +- **Jul 2024 (v2.1):** BigVGAN is now integrated with 🤗 Hugging Face Hub with easy access to inference using pretrained checkpoints. We also provide an interactive demo on Hugging Face Spaces. + +- **Jul 2024 (v2):** We release BigVGAN-v2 along with pretrained checkpoints. Below are the highlights: + - Custom CUDA kernel for inference: we provide a fused upsampling + activation kernel written in CUDA for accelerated inference speed. Our test shows 1.5 - 3x faster speed on a single A100 GPU. + - Improved discriminator and loss: BigVGAN-v2 is trained using a [multi-scale sub-band CQT discriminator](https://arxiv.org/abs/2311.14957) and a [multi-scale mel spectrogram loss](https://arxiv.org/abs/2306.06546). + - Larger training data: BigVGAN-v2 is trained using datasets containing diverse audio types, including speech in multiple languages, environmental sounds, and instruments. + - We provide pretrained checkpoints of BigVGAN-v2 using diverse audio configurations, supporting up to 44 kHz sampling rate and 512x upsampling ratio. + +## Installation + +The codebase has been tested on Python `3.10` and PyTorch `2.3.1` conda packages with either `pytorch-cuda=12.1` or `pytorch-cuda=11.8`. Below is an example command to create the conda environment: + +```shell +conda create -n bigvgan python=3.10 pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia +conda activate bigvgan +``` + +Clone the repository and install dependencies: + +```shell +git clone https://github.com/NVIDIA/BigVGAN +cd BigVGAN +pip install -r requirements.txt +``` + +## Inference Quickstart using 🤗 Hugging Face Hub + +Below example describes how you can use BigVGAN: load the pretrained BigVGAN generator from Hugging Face Hub, compute mel spectrogram from input waveform, and generate synthesized waveform using the mel spectrogram as the model's input. + +```python +device = 'cuda' + +import torch +import bigvgan +import librosa +from meldataset import get_mel_spectrogram + +# instantiate the model. You can optionally set use_cuda_kernel=True for faster inference. +model = bigvgan.BigVGAN.from_pretrained('nvidia/bigvgan_v2_24khz_100band_256x', use_cuda_kernel=False) + +# remove weight norm in the model and set to eval mode +model.remove_weight_norm() +model = model.eval().to(device) + +# load wav file and compute mel spectrogram +wav_path = '/path/to/your/audio.wav' +wav, sr = librosa.load(wav_path, sr=model.h.sampling_rate, mono=True) # wav is np.ndarray with shape [T_time] and values in [-1, 1] +wav = torch.FloatTensor(wav).unsqueeze(0) # wav is FloatTensor with shape [B(1), T_time] + +# compute mel spectrogram from the ground truth audio +mel = get_mel_spectrogram(wav, model.h).to(device) # mel is FloatTensor with shape [B(1), C_mel, T_frame] + +# generate waveform from mel +with torch.inference_mode(): + wav_gen = model(mel) # wav_gen is FloatTensor with shape [B(1), 1, T_time] and values in [-1, 1] +wav_gen_float = wav_gen.squeeze(0).cpu() # wav_gen is FloatTensor with shape [1, T_time] + +# you can convert the generated waveform to 16 bit linear PCM +wav_gen_int16 = (wav_gen_float * 32767.0).numpy().astype('int16') # wav_gen is now np.ndarray with shape [1, T_time] and int16 dtype +``` + +## Local gradio demo + +You can run a local gradio demo using below command: + +```python +pip install -r demo/requirements.txt +python demo/app.py +``` + +## Training + +Create symbolic link to the root of the dataset. The codebase uses filelist with the relative path from the dataset. Below are the example commands for LibriTTS dataset: + +```shell +cd filelists/LibriTTS && \ +ln -s /path/to/your/LibriTTS/train-clean-100 train-clean-100 && \ +ln -s /path/to/your/LibriTTS/train-clean-360 train-clean-360 && \ +ln -s /path/to/your/LibriTTS/train-other-500 train-other-500 && \ +ln -s /path/to/your/LibriTTS/dev-clean dev-clean && \ +ln -s /path/to/your/LibriTTS/dev-other dev-other && \ +ln -s /path/to/your/LibriTTS/test-clean test-clean && \ +ln -s /path/to/your/LibriTTS/test-other test-other && \ +cd ../.. +``` + +Train BigVGAN model. Below is an example command for training BigVGAN-v2 using LibriTTS dataset at 24kHz with a full 100-band mel spectrogram as input: + +```shell +python train.py \ +--config configs/bigvgan_v2_24khz_100band_256x.json \ +--input_wavs_dir filelists/LibriTTS \ +--input_training_file filelists/LibriTTS/train-full.txt \ +--input_validation_file filelists/LibriTTS/val-full.txt \ +--list_input_unseen_wavs_dir filelists/LibriTTS filelists/LibriTTS \ +--list_input_unseen_validation_file filelists/LibriTTS/dev-clean.txt filelists/LibriTTS/dev-other.txt \ +--checkpoint_path exp/bigvgan_v2_24khz_100band_256x +``` + +## Synthesis + +Synthesize from BigVGAN model. Below is an example command for generating audio from the model. +It computes mel spectrograms using wav files from `--input_wavs_dir` and saves the generated audio to `--output_dir`. + +```shell +python inference.py \ +--checkpoint_file /path/to/your/bigvgan_v2_24khz_100band_256x/bigvgan_generator.pt \ +--input_wavs_dir /path/to/your/input_wav \ +--output_dir /path/to/your/output_wav +``` + +`inference_e2e.py` supports synthesis directly from the mel spectrogram saved in `.npy` format, with shapes `[1, channel, frame]` or `[channel, frame]`. +It loads mel spectrograms from `--input_mels_dir` and saves the generated audio to `--output_dir`. + +Make sure that the STFT hyperparameters for mel spectrogram are the same as the model, which are defined in `config.json` of the corresponding model. + +```shell +python inference_e2e.py \ +--checkpoint_file /path/to/your/bigvgan_v2_24khz_100band_256x/bigvgan_generator.pt \ +--input_mels_dir /path/to/your/input_mel \ +--output_dir /path/to/your/output_wav +``` + +## Using Custom CUDA Kernel for Synthesis + +You can apply the fast CUDA inference kernel by using a parameter `use_cuda_kernel` when instantiating BigVGAN: + +```python +generator = BigVGAN(h, use_cuda_kernel=True) +``` + +You can also pass `--use_cuda_kernel` to `inference.py` and `inference_e2e.py` to enable this feature. + +When applied for the first time, it builds the kernel using `nvcc` and `ninja`. If the build succeeds, the kernel is saved to `alias_free_activation/cuda/build` and the model automatically loads the kernel. The codebase has been tested using CUDA `12.1`. + +Please make sure that both are installed in your system and `nvcc` installed in your system matches the version your PyTorch build is using. + +We recommend running `test_cuda_vs_torch_model.py` first to build and check the correctness of the CUDA kernel. See below example command and its output, where it returns `[Success] test CUDA fused vs. plain torch BigVGAN inference`: + +```python +python tests/test_cuda_vs_torch_model.py \ +--checkpoint_file /path/to/your/bigvgan_generator.pt +``` + +```shell +loading plain Pytorch BigVGAN +... +loading CUDA kernel BigVGAN with auto-build +Detected CUDA files, patching ldflags +Emitting ninja build file /path/to/your/BigVGAN/alias_free_activation/cuda/build/build.ninja.. +Building extension module anti_alias_activation_cuda... +... +Loading extension module anti_alias_activation_cuda... +... +Loading '/path/to/your/bigvgan_generator.pt' +... +[Success] test CUDA fused vs. plain torch BigVGAN inference + > mean_difference=0.0007238413265440613 +... +``` + +If you see `[Fail] test CUDA fused vs. plain torch BigVGAN inference`, it means that the CUDA kernel inference is incorrect. Please check if `nvcc` installed in your system is compatible with your PyTorch version. + +## Pretrained Models + +We provide the [pretrained models on Hugging Face Collections](https://huggingface.co/collections/nvidia/bigvgan-66959df3d97fd7d98d97dc9a). +One can download the checkpoints of the generator weight (named `bigvgan_generator.pt`) and its discriminator/optimizer states (named `bigvgan_discriminator_optimizer.pt`) within the listed model repositories. + +| Model Name | Sampling Rate | Mel band | fmax | Upsampling Ratio | Params | Dataset | Steps | Fine-Tuned | +|:--------------------------------------------------------------------------------------------------------:|:-------------:|:--------:|:-----:|:----------------:|:------:|:--------------------------:|:-----:|:----------:| +| [bigvgan_v2_44khz_128band_512x](https://huggingface.co/nvidia/bigvgan_v2_44khz_128band_512x) | 44 kHz | 128 | 22050 | 512 | 122M | Large-scale Compilation | 5M | No | +| [bigvgan_v2_44khz_128band_256x](https://huggingface.co/nvidia/bigvgan_v2_44khz_128band_256x) | 44 kHz | 128 | 22050 | 256 | 112M | Large-scale Compilation | 5M | No | +| [bigvgan_v2_24khz_100band_256x](https://huggingface.co/nvidia/bigvgan_v2_24khz_100band_256x) | 24 kHz | 100 | 12000 | 256 | 112M | Large-scale Compilation | 5M | No | +| [bigvgan_v2_22khz_80band_256x](https://huggingface.co/nvidia/bigvgan_v2_22khz_80band_256x) | 22 kHz | 80 | 11025 | 256 | 112M | Large-scale Compilation | 5M | No | +| [bigvgan_v2_22khz_80band_fmax8k_256x](https://huggingface.co/nvidia/bigvgan_v2_22khz_80band_fmax8k_256x) | 22 kHz | 80 | 8000 | 256 | 112M | Large-scale Compilation | 5M | No | +| [bigvgan_24khz_100band](https://huggingface.co/nvidia/bigvgan_24khz_100band) | 24 kHz | 100 | 12000 | 256 | 112M | LibriTTS | 5M | No | +| [bigvgan_base_24khz_100band](https://huggingface.co/nvidia/bigvgan_base_24khz_100band) | 24 kHz | 100 | 12000 | 256 | 14M | LibriTTS | 5M | No | +| [bigvgan_22khz_80band](https://huggingface.co/nvidia/bigvgan_22khz_80band) | 22 kHz | 80 | 8000 | 256 | 112M | LibriTTS + VCTK + LJSpeech | 5M | No | +| [bigvgan_base_22khz_80band](https://huggingface.co/nvidia/bigvgan_base_22khz_80band) | 22 kHz | 80 | 8000 | 256 | 14M | LibriTTS + VCTK + LJSpeech | 5M | No | + +The paper results are based on the original 24kHz BigVGAN models (`bigvgan_24khz_100band` and `bigvgan_base_24khz_100band`) trained on LibriTTS dataset. +We also provide 22kHz BigVGAN models with band-limited setup (i.e., fmax=8000) for TTS applications. +Note that the checkpoints use `snakebeta` activation with log scale parameterization, which have the best overall quality. + +You can fine-tune the models by: + +1. downloading the checkpoints (both the generator weight and its discriminator/optimizer states) +2. resuming training using your audio dataset by specifying `--checkpoint_path` that includes the checkpoints when launching `train.py` + +## Training Details of BigVGAN-v2 + +Comapred to the original BigVGAN, the pretrained checkpoints of BigVGAN-v2 used `batch_size=32` with a longer `segment_size=65536` and are trained using 8 A100 GPUs. + +Note that the BigVGAN-v2 `json` config files in `./configs` use `batch_size=4` as default to fit in a single A100 GPU for training. You can fine-tune the models adjusting `batch_size` depending on your GPUs. + +When training BigVGAN-v2 from scratch with small batch size, it can potentially encounter the early divergence problem mentioned in the paper. In such case, we recommend lowering the `clip_grad_norm` value (e.g. `100`) for the early training iterations (e.g. 20000 steps) and increase the value to the default `500`. + +## Evaluation Results of BigVGAN-v2 + +Below are the objective results of the 24kHz model (`bigvgan_v2_24khz_100band_256x`) obtained from the LibriTTS `dev` sets. BigVGAN-v2 shows noticeable improvements of the metrics. The model also exhibits reduced perceptual artifacts, especially for non-speech audio. + +| Model | Dataset | Steps | PESQ(↑) | M-STFT(↓) | MCD(↓) | Periodicity(↓) | V/UV F1(↑) | +|:----------:|:-----------------------:|:-----:|:---------:|:----------:|:----------:|:--------------:|:----------:| +| BigVGAN | LibriTTS | 1M | 4.027 | 0.7997 | 0.3745 | 0.1018 | 0.9598 | +| BigVGAN | LibriTTS | 5M | 4.256 | 0.7409 | 0.2988 | 0.0809 | 0.9698 | +| BigVGAN-v2 | Large-scale Compilation | 3M | 4.359 | 0.7134 | 0.3060 | 0.0621 | 0.9777 | +| BigVGAN-v2 | Large-scale Compilation | 5M | **4.362** | **0.7026** | **0.2903** | **0.0593** | **0.9793** | + +## Speed Benchmark + +Below are the speed and VRAM usage benchmark results of BigVGAN from `tests/test_cuda_vs_torch_model.py`, using `bigvgan_v2_24khz_100band_256x` as a reference model. + +| GPU | num_mel_frame | use_cuda_kernel | Speed (kHz) | Real-time Factor | VRAM (GB) | +|:--------------------------:|:-------------:|:---------------:|:-----------:|:----------------:|:---------:| +| NVIDIA A100 | 256 | False | 1672.1 | 69.7x | 1.3 | +| | | True | 3916.5 | 163.2x | 1.3 | +| | 2048 | False | 1899.6 | 79.2x | 1.7 | +| | | True | 5330.1 | 222.1x | 1.7 | +| | 16384 | False | 1973.8 | 82.2x | 5.0 | +| | | True | 5761.7 | 240.1x | 4.4 | +| NVIDIA GeForce RTX 3080 | 256 | False | 841.1 | 35.0x | 1.3 | +| | | True | 1598.1 | 66.6x | 1.3 | +| | 2048 | False | 929.9 | 38.7x | 1.7 | +| | | True | 1971.3 | 82.1x | 1.6 | +| | 16384 | False | 943.4 | 39.3x | 5.0 | +| | | True | 2026.5 | 84.4x | 3.9 | +| NVIDIA GeForce RTX 2080 Ti | 256 | False | 515.6 | 21.5x | 1.3 | +| | | True | 811.3 | 33.8x | 1.3 | +| | 2048 | False | 576.5 | 24.0x | 1.7 | +| | | True | 1023.0 | 42.6x | 1.5 | +| | 16384 | False | 589.4 | 24.6x | 5.0 | +| | | True | 1068.1 | 44.5x | 3.2 | + +## Acknowledgements + +We thank Vijay Anand Korthikanti and Kevin J. Shih for their generous support in implementing the CUDA kernel for inference. + +## References + +- [HiFi-GAN](https://github.com/jik876/hifi-gan) (for generator and multi-period discriminator) +- [Snake](https://github.com/EdwardDixon/snake) (for periodic activation) +- [Alias-free-torch](https://github.com/junjun3518/alias-free-torch) (for anti-aliasing) +- [Julius](https://github.com/adefossez/julius) (for low-pass filter) +- [UnivNet](https://github.com/mindslab-ai/univnet) (for multi-resolution discriminator) +- [descript-audio-codec](https://github.com/descriptinc/descript-audio-codec) and [vocos](https://github.com/gemelo-ai/vocos) (for multi-band multi-scale STFT discriminator and multi-scale mel spectrogram loss) +- [Amphion](https://github.com/open-mmlab/Amphion) (for multi-scale sub-band CQT discriminator) diff --git a/src/third_party/BigVGAN/activations.py b/src/third_party/BigVGAN/activations.py new file mode 100644 index 0000000000000000000000000000000000000000..b1e83fcdef46300f5d5bff1f0dbf71f58f3b1186 --- /dev/null +++ b/src/third_party/BigVGAN/activations.py @@ -0,0 +1,126 @@ +# 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 diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/__init__.py b/src/third_party/BigVGAN/alias_free_activation/cuda/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/activation1d.py b/src/third_party/BigVGAN/alias_free_activation/cuda/activation1d.py new file mode 100644 index 0000000000000000000000000000000000000000..fc0d313cb265170943fb7cb16742b031038f7859 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/activation1d.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import torch +import torch.nn as nn +from alias_free_activation.torch.resample import UpSample1d, DownSample1d + +# load fused CUDA kernel: this enables importing anti_alias_activation_cuda +from alias_free_activation.cuda import load + +anti_alias_activation_cuda = load.load() + + +class FusedAntiAliasActivation(torch.autograd.Function): + """ + Assumes filter size 12, replication padding on upsampling/downsampling, and logscale alpha/beta parameters as inputs. + The hyperparameters are hard-coded in the kernel to maximize speed. + NOTE: The fused kenrel is incorrect for Activation1d with different hyperparameters. + """ + + @staticmethod + def forward(ctx, inputs, up_ftr, down_ftr, alpha, beta): + activation_results = anti_alias_activation_cuda.forward( + inputs, up_ftr, down_ftr, alpha, beta + ) + + return activation_results + + @staticmethod + def backward(ctx, output_grads): + raise NotImplementedError + return output_grads, None, None + + +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, + fused: bool = True, + ): + 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) + + self.fused = fused # Whether to use fused CUDA kernel or not + + def forward(self, x): + if not self.fused: + x = self.upsample(x) + x = self.act(x) + x = self.downsample(x) + return x + else: + if self.act.__class__.__name__ == "Snake": + beta = self.act.alpha.data # Snake uses same params for alpha and beta + else: + beta = ( + self.act.beta.data + ) # Snakebeta uses different params for alpha and beta + alpha = self.act.alpha.data + if ( + not self.act.alpha_logscale + ): # Exp baked into cuda kernel, cancel it out with a log + alpha = torch.log(alpha) + beta = torch.log(beta) + + x = FusedAntiAliasActivation.apply( + x, self.upsample.filter, self.downsample.lowpass.filter, alpha, beta + ) + return x diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation.cpp b/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..94fd90da386e66ce12a64ef243e4d125926dfd2a --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation.cpp @@ -0,0 +1,23 @@ +/* coding=utf-8 + * Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + #include + +extern "C" torch::Tensor fwd_cuda(torch::Tensor const &input, torch::Tensor const &up_filter, torch::Tensor const &down_filter, torch::Tensor const &alpha, torch::Tensor const &beta); + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("forward", &fwd_cuda, "Anti-Alias Activation forward (CUDA)"); +} \ No newline at end of file diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation_cuda.cu b/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation_cuda.cu new file mode 100644 index 0000000000000000000000000000000000000000..7ee97492984a92c753f2357f03e7c04252060824 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/anti_alias_activation_cuda.cu @@ -0,0 +1,246 @@ +/* coding=utf-8 + * Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include "type_shim.h" +#include +#include +#include +#include +#include + +namespace +{ + // Hard-coded hyperparameters + // WARP_SIZE and WARP_BATCH must match the return values batches_per_warp and + constexpr int ELEMENTS_PER_LDG_STG = 1; //(WARP_ITERATIONS < 4) ? 1 : 4; + constexpr int BUFFER_SIZE = 32; + constexpr int FILTER_SIZE = 12; + constexpr int HALF_FILTER_SIZE = 6; + constexpr int UPSAMPLE_REPLICATION_PAD = 5; // 5 on each side, matching torch impl + constexpr int DOWNSAMPLE_REPLICATION_PAD_LEFT = 5; // matching torch impl + constexpr int DOWNSAMPLE_REPLICATION_PAD_RIGHT = 6; // matching torch impl + + template + __global__ void anti_alias_activation_forward( + output_t *dst, + const input_t *src, + const input_t *up_ftr, + const input_t *down_ftr, + const input_t *alpha, + const input_t *beta, + int batch_size, + int channels, + int seq_len) + { + // Up and downsample filters + input_t up_filter[FILTER_SIZE]; + input_t down_filter[FILTER_SIZE]; + + // Load data from global memory including extra indices reserved for replication paddings + input_t elements[2 * FILTER_SIZE + 2 * BUFFER_SIZE + 2 * UPSAMPLE_REPLICATION_PAD] = {0}; + input_t intermediates[2 * FILTER_SIZE + 2 * BUFFER_SIZE + DOWNSAMPLE_REPLICATION_PAD_LEFT + DOWNSAMPLE_REPLICATION_PAD_RIGHT] = {0}; + + // Output stores downsampled output before writing to dst + output_t output[BUFFER_SIZE]; + + // blockDim/threadIdx = (128, 1, 1) + // gridDim/blockIdx = (seq_blocks, channels, batches) + int block_offset = (blockIdx.x * 128 * BUFFER_SIZE + seq_len * (blockIdx.y + gridDim.y * blockIdx.z)); + int local_offset = threadIdx.x * BUFFER_SIZE; + int seq_offset = blockIdx.x * 128 * BUFFER_SIZE + local_offset; + + // intermediate have double the seq_len + int intermediate_local_offset = threadIdx.x * BUFFER_SIZE * 2; + int intermediate_seq_offset = blockIdx.x * 128 * BUFFER_SIZE * 2 + intermediate_local_offset; + + // Get values needed for replication padding before moving pointer + const input_t *right_most_pntr = src + (seq_len * (blockIdx.y + gridDim.y * blockIdx.z)); + input_t seq_left_most_value = right_most_pntr[0]; + input_t seq_right_most_value = right_most_pntr[seq_len - 1]; + + // Move src and dst pointers + src += block_offset + local_offset; + dst += block_offset + local_offset; + + // Alpha and beta values for snake activatons. Applies exp by default + alpha = alpha + blockIdx.y; + input_t alpha_val = expf(alpha[0]); + beta = beta + blockIdx.y; + input_t beta_val = expf(beta[0]); + + #pragma unroll + for (int it = 0; it < FILTER_SIZE; it += 1) + { + up_filter[it] = up_ftr[it]; + down_filter[it] = down_ftr[it]; + } + + // Apply replication padding for upsampling, matching torch impl + #pragma unroll + for (int it = -HALF_FILTER_SIZE; it < BUFFER_SIZE + HALF_FILTER_SIZE; it += 1) + { + int element_index = seq_offset + it; // index for element + if ((element_index < 0) && (element_index >= -UPSAMPLE_REPLICATION_PAD)) + { + elements[2 * (HALF_FILTER_SIZE + it)] = 2 * seq_left_most_value; + } + if ((element_index >= seq_len) && (element_index < seq_len + UPSAMPLE_REPLICATION_PAD)) + { + elements[2 * (HALF_FILTER_SIZE + it)] = 2 * seq_right_most_value; + } + if ((element_index >= 0) && (element_index < seq_len)) + { + elements[2 * (HALF_FILTER_SIZE + it)] = 2 * src[it]; + } + } + + // Apply upsampling strided convolution and write to intermediates. It reserves DOWNSAMPLE_REPLICATION_PAD_LEFT for replication padding of the downsampilng conv later + #pragma unroll + for (int it = 0; it < (2 * BUFFER_SIZE + 2 * FILTER_SIZE); it += 1) + { + input_t acc = 0.0; + int element_index = intermediate_seq_offset + it; // index for intermediate + #pragma unroll + for (int f_idx = 0; f_idx < FILTER_SIZE; f_idx += 1) + { + if ((element_index + f_idx) >= 0) + { + acc += up_filter[f_idx] * elements[it + f_idx]; + } + } + intermediates[it + DOWNSAMPLE_REPLICATION_PAD_LEFT] = acc; + } + + // Apply activation function. It reserves DOWNSAMPLE_REPLICATION_PAD_LEFT and DOWNSAMPLE_REPLICATION_PAD_RIGHT for replication padding of the downsampilng conv later + double no_div_by_zero = 0.000000001; + #pragma unroll + for (int it = 0; it < 2 * BUFFER_SIZE + 2 * FILTER_SIZE; it += 1) + { + intermediates[it + DOWNSAMPLE_REPLICATION_PAD_LEFT] += (1.0 / (beta_val + no_div_by_zero)) * sinf(intermediates[it + DOWNSAMPLE_REPLICATION_PAD_LEFT] * alpha_val) * sinf(intermediates[it + DOWNSAMPLE_REPLICATION_PAD_LEFT] * alpha_val); + } + + // Apply replication padding before downsampling conv from intermediates + #pragma unroll + for (int it = 0; it < DOWNSAMPLE_REPLICATION_PAD_LEFT; it += 1) + { + intermediates[it] = intermediates[DOWNSAMPLE_REPLICATION_PAD_LEFT]; + } + #pragma unroll + for (int it = DOWNSAMPLE_REPLICATION_PAD_LEFT + 2 * BUFFER_SIZE + 2 * FILTER_SIZE; it < DOWNSAMPLE_REPLICATION_PAD_LEFT + 2 * BUFFER_SIZE + 2 * FILTER_SIZE + DOWNSAMPLE_REPLICATION_PAD_RIGHT; it += 1) + { + intermediates[it] = intermediates[DOWNSAMPLE_REPLICATION_PAD_LEFT + 2 * BUFFER_SIZE + 2 * FILTER_SIZE - 1]; + } + + // Apply downsample strided convolution (assuming stride=2) from intermediates + #pragma unroll + for (int it = 0; it < BUFFER_SIZE; it += 1) + { + input_t acc = 0.0; + #pragma unroll + for (int f_idx = 0; f_idx < FILTER_SIZE; f_idx += 1) + { + // Add constant DOWNSAMPLE_REPLICATION_PAD_RIGHT to match torch implementation + acc += down_filter[f_idx] * intermediates[it * 2 + f_idx + DOWNSAMPLE_REPLICATION_PAD_RIGHT]; + } + output[it] = acc; + } + + // Write output to dst + #pragma unroll + for (int it = 0; it < BUFFER_SIZE; it += ELEMENTS_PER_LDG_STG) + { + int element_index = seq_offset + it; + if (element_index < seq_len) + { + dst[it] = output[it]; + } + } + + } + + template + void dispatch_anti_alias_activation_forward( + output_t *dst, + const input_t *src, + const input_t *up_ftr, + const input_t *down_ftr, + const input_t *alpha, + const input_t *beta, + int batch_size, + int channels, + int seq_len) + { + if (seq_len == 0) + { + return; + } + else + { + // Use 128 threads per block to maximimize gpu utilization + constexpr int threads_per_block = 128; + constexpr int seq_len_per_block = 4096; + int blocks_per_seq_len = (seq_len + seq_len_per_block - 1) / seq_len_per_block; + dim3 blocks(blocks_per_seq_len, channels, batch_size); + dim3 threads(threads_per_block, 1, 1); + + anti_alias_activation_forward + <<>>(dst, src, up_ftr, down_ftr, alpha, beta, batch_size, channels, seq_len); + } + } +} + +extern "C" torch::Tensor fwd_cuda(torch::Tensor const &input, torch::Tensor const &up_filter, torch::Tensor const &down_filter, torch::Tensor const &alpha, torch::Tensor const &beta) +{ + // Input is a 3d tensor with dimensions [batches, channels, seq_len] + const int batches = input.size(0); + const int channels = input.size(1); + const int seq_len = input.size(2); + + // Output + auto act_options = input.options().requires_grad(false); + + torch::Tensor anti_alias_activation_results = + torch::empty({batches, channels, seq_len}, act_options); + + void *input_ptr = static_cast(input.data_ptr()); + void *up_filter_ptr = static_cast(up_filter.data_ptr()); + void *down_filter_ptr = static_cast(down_filter.data_ptr()); + void *alpha_ptr = static_cast(alpha.data_ptr()); + void *beta_ptr = static_cast(beta.data_ptr()); + void *anti_alias_activation_results_ptr = static_cast(anti_alias_activation_results.data_ptr()); + + DISPATCH_FLOAT_HALF_AND_BFLOAT( + input.scalar_type(), + "dispatch anti alias activation_forward", + dispatch_anti_alias_activation_forward( + reinterpret_cast(anti_alias_activation_results_ptr), + reinterpret_cast(input_ptr), + reinterpret_cast(up_filter_ptr), + reinterpret_cast(down_filter_ptr), + reinterpret_cast(alpha_ptr), + reinterpret_cast(beta_ptr), + batches, + channels, + seq_len);); + return anti_alias_activation_results; +} \ No newline at end of file diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/compat.h b/src/third_party/BigVGAN/alias_free_activation/cuda/compat.h new file mode 100644 index 0000000000000000000000000000000000000000..0f93af5700470e7f6066af7dbe56aced98ea32d9 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/compat.h @@ -0,0 +1,29 @@ +/* coding=utf-8 + * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*This code is copied fron NVIDIA apex: + * https://github.com/NVIDIA/apex + * with minor changes. */ + +#ifndef TORCH_CHECK +#define TORCH_CHECK AT_CHECK +#endif + +#ifdef VERSION_GE_1_3 +#define DATA_PTR data_ptr +#else +#define DATA_PTR data +#endif diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/load.py b/src/third_party/BigVGAN/alias_free_activation/cuda/load.py new file mode 100644 index 0000000000000000000000000000000000000000..82afde3d73dda72b06af28a622fdab1954825a28 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/load.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import os +import pathlib +import subprocess + +from torch.utils import cpp_extension + +""" +Setting this param to a list has a problem of generating different compilation commands (with diferent order of architectures) and leading to recompilation of fused kernels. +Set it to empty stringo avoid recompilation and assign arch flags explicity in extra_cuda_cflags below +""" +os.environ["TORCH_CUDA_ARCH_LIST"] = "" + + +def load(): + # Check if cuda 11 is installed for compute capability 8.0 + cc_flag = [] + _, bare_metal_major, _ = _get_cuda_bare_metal_version(cpp_extension.CUDA_HOME) + if int(bare_metal_major) >= 11: + cc_flag.append("-gencode") + cc_flag.append("arch=compute_80,code=sm_80") + + # Build path + srcpath = pathlib.Path(__file__).parent.absolute() + buildpath = srcpath / "build" + _create_build_dir(buildpath) + + # Helper function to build the kernels. + def _cpp_extention_load_helper(name, sources, extra_cuda_flags): + return cpp_extension.load( + name=name, + sources=sources, + build_directory=buildpath, + extra_cflags=[ + "-O3", + ], + extra_cuda_cflags=[ + "-O3", + "-gencode", + "arch=compute_70,code=sm_70", + "--use_fast_math", + ] + + extra_cuda_flags + + cc_flag, + verbose=True, + ) + + extra_cuda_flags = [ + "-U__CUDA_NO_HALF_OPERATORS__", + "-U__CUDA_NO_HALF_CONVERSIONS__", + "--expt-relaxed-constexpr", + "--expt-extended-lambda", + ] + + sources = [ + srcpath / "anti_alias_activation.cpp", + srcpath / "anti_alias_activation_cuda.cu", + ] + anti_alias_activation_cuda = _cpp_extention_load_helper( + "anti_alias_activation_cuda", sources, extra_cuda_flags + ) + + return anti_alias_activation_cuda + + +def _get_cuda_bare_metal_version(cuda_dir): + raw_output = subprocess.check_output( + [cuda_dir + "/bin/nvcc", "-V"], universal_newlines=True + ) + output = raw_output.split() + release_idx = output.index("release") + 1 + release = output[release_idx].split(".") + bare_metal_major = release[0] + bare_metal_minor = release[1][0] + + return raw_output, bare_metal_major, bare_metal_minor + + +def _create_build_dir(buildpath): + try: + os.mkdir(buildpath) + except OSError: + if not os.path.isdir(buildpath): + print(f"Creation of the build directory {buildpath} failed") diff --git a/src/third_party/BigVGAN/alias_free_activation/cuda/type_shim.h b/src/third_party/BigVGAN/alias_free_activation/cuda/type_shim.h new file mode 100644 index 0000000000000000000000000000000000000000..4328d0369a5fb8730cdf236d9f267453f4201d84 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/cuda/type_shim.h @@ -0,0 +1,92 @@ +/* coding=utf-8 + * Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "compat.h" + +#define DISPATCH_FLOAT_HALF_AND_BFLOAT(TYPE, NAME, ...) \ + switch (TYPE) \ + { \ + case at::ScalarType::Float: \ + { \ + using scalar_t = float; \ + __VA_ARGS__; \ + break; \ + } \ + case at::ScalarType::Half: \ + { \ + using scalar_t = at::Half; \ + __VA_ARGS__; \ + break; \ + } \ + case at::ScalarType::BFloat16: \ + { \ + using scalar_t = at::BFloat16; \ + __VA_ARGS__; \ + break; \ + } \ + default: \ + AT_ERROR(#NAME, " not implemented for '", toString(TYPE), "'"); \ + } + +#define DISPATCH_FLOAT_HALF_AND_BFLOAT_INOUT_TYPES(TYPEIN, TYPEOUT, NAME, ...) \ + switch (TYPEIN) \ + { \ + case at::ScalarType::Float: \ + { \ + using scalar_t_in = float; \ + switch (TYPEOUT) \ + { \ + case at::ScalarType::Float: \ + { \ + using scalar_t_out = float; \ + __VA_ARGS__; \ + break; \ + } \ + case at::ScalarType::Half: \ + { \ + using scalar_t_out = at::Half; \ + __VA_ARGS__; \ + break; \ + } \ + case at::ScalarType::BFloat16: \ + { \ + using scalar_t_out = at::BFloat16; \ + __VA_ARGS__; \ + break; \ + } \ + default: \ + AT_ERROR(#NAME, " not implemented for '", toString(TYPEOUT), "'"); \ + } \ + break; \ + } \ + case at::ScalarType::Half: \ + { \ + using scalar_t_in = at::Half; \ + using scalar_t_out = at::Half; \ + __VA_ARGS__; \ + break; \ + } \ + case at::ScalarType::BFloat16: \ + { \ + using scalar_t_in = at::BFloat16; \ + using scalar_t_out = at::BFloat16; \ + __VA_ARGS__; \ + break; \ + } \ + default: \ + AT_ERROR(#NAME, " not implemented for '", toString(TYPEIN), "'"); \ + } diff --git a/src/third_party/BigVGAN/alias_free_activation/torch/__init__.py b/src/third_party/BigVGAN/alias_free_activation/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7bb0ad84ef184dcb15464c8ca827ae1c284f8990 --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/torch/__init__.py @@ -0,0 +1,6 @@ +# 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 * diff --git a/src/third_party/BigVGAN/alias_free_activation/torch/act.py b/src/third_party/BigVGAN/alias_free_activation/torch/act.py new file mode 100644 index 0000000000000000000000000000000000000000..421a1cdd470e462b263c50db4d82bad2d0fe552e --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/torch/act.py @@ -0,0 +1,30 @@ +# 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 alias_free_activation.torch.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 diff --git a/src/third_party/BigVGAN/alias_free_activation/torch/filter.py b/src/third_party/BigVGAN/alias_free_activation/torch/filter.py new file mode 100644 index 0000000000000000000000000000000000000000..81a4a9a7cefb457f8880f54385335180fbd43f1b --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/torch/filter.py @@ -0,0 +1,101 @@ +# 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.0, 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.0: + beta = 0.1102 * (A - 8.7) + elif A >= 21.0: + beta = 0.5842 * (A - 21) ** 0.4 + 0.07886 * (A - 21.0) + else: + beta = 0.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.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 diff --git a/src/third_party/BigVGAN/alias_free_activation/torch/resample.py b/src/third_party/BigVGAN/alias_free_activation/torch/resample.py new file mode 100644 index 0000000000000000000000000000000000000000..c2cf40e9a24da7da3b707659cec274860102a11f --- /dev/null +++ b/src/third_party/BigVGAN/alias_free_activation/torch/resample.py @@ -0,0 +1,58 @@ +# 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 alias_free_activation.torch.filter import LowPassFilter1d +from alias_free_activation.torch.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 diff --git a/src/third_party/BigVGAN/bigvgan.py b/src/third_party/BigVGAN/bigvgan.py new file mode 100644 index 0000000000000000000000000000000000000000..40407c3ca142bf7e069aba066486cfe1b3ab9016 --- /dev/null +++ b/src/third_party/BigVGAN/bigvgan.py @@ -0,0 +1,493 @@ +# Copyright (c) 2024 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 os +import json +from pathlib import Path +from typing import Optional, Union, Dict + +import torch +import torch.nn as nn +from torch.nn import Conv1d, ConvTranspose1d +from torch.nn.utils import weight_norm, remove_weight_norm + +import activations +from utils import init_weights, get_padding +from alias_free_activation.torch.act import Activation1d as TorchActivation1d +from env import AttrDict + +from huggingface_hub import PyTorchModelHubMixin, hf_hub_download + + +def load_hparams_from_json(path) -> AttrDict: + with open(path) as f: + data = f.read() + return AttrDict(json.loads(data)) + + +class AMPBlock1(torch.nn.Module): + """ + AMPBlock applies Snake / SnakeBeta activation functions with trainable parameters that control periodicity, defined for each layer. + AMPBlock1 has additional self.convs2 that contains additional Conv1d layers with a fixed dilation=1 followed by each layer in self.convs1 + + Args: + h (AttrDict): Hyperparameters. + channels (int): Number of convolution channels. + kernel_size (int): Size of the convolution kernel. Default is 3. + dilation (tuple): Dilation rates for the convolutions. Each dilation layer has two convolutions. Default is (1, 3, 5). + activation (str): Activation function type. Should be either 'snake' or 'snakebeta'. Default is None. + """ + + def __init__( + self, + h: AttrDict, + channels: int, + kernel_size: int = 3, + dilation: tuple = (1, 3, 5), + activation: str = None, + ): + super().__init__() + + self.h = h + + self.convs1 = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + stride=1, + dilation=d, + padding=get_padding(kernel_size, d), + ) + ) + for d in dilation + ] + ) + self.convs1.apply(init_weights) + + self.convs2 = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + stride=1, + dilation=1, + padding=get_padding(kernel_size, 1), + ) + ) + for _ in range(len(dilation)) + ] + ) + self.convs2.apply(init_weights) + + self.num_layers = len(self.convs1) + len( + self.convs2 + ) # Total number of conv layers + + # Select which Activation1d, lazy-load cuda version to ensure backward compatibility + if self.h.get("use_cuda_kernel", False): + from alias_free_activation.cuda.activation1d import ( + Activation1d as CudaActivation1d, + ) + + Activation1d = CudaActivation1d + else: + Activation1d = TorchActivation1d + + # Activation functions + if activation == "snake": + self.activations = nn.ModuleList( + [ + Activation1d( + activation=activations.Snake( + channels, alpha_logscale=h.snake_logscale + ) + ) + for _ in range(self.num_layers) + ] + ) + elif activation == "snakebeta": + 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): + """ + AMPBlock applies Snake / SnakeBeta activation functions with trainable parameters that control periodicity, defined for each layer. + Unlike AMPBlock1, AMPBlock2 does not contain extra Conv1d layers with fixed dilation=1 + + Args: + h (AttrDict): Hyperparameters. + channels (int): Number of convolution channels. + kernel_size (int): Size of the convolution kernel. Default is 3. + dilation (tuple): Dilation rates for the convolutions. Each dilation layer has two convolutions. Default is (1, 3, 5). + activation (str): Activation function type. Should be either 'snake' or 'snakebeta'. Default is None. + """ + + def __init__( + self, + h: AttrDict, + channels: int, + kernel_size: int = 3, + dilation: tuple = (1, 3, 5), + activation: str = None, + ): + super().__init__() + + self.h = h + + self.convs = nn.ModuleList( + [ + weight_norm( + Conv1d( + channels, + channels, + kernel_size, + stride=1, + dilation=d, + padding=get_padding(kernel_size, d), + ) + ) + for d in dilation + ] + ) + self.convs.apply(init_weights) + + self.num_layers = len(self.convs) # Total number of conv layers + + # Select which Activation1d, lazy-load cuda version to ensure backward compatibility + if self.h.get("use_cuda_kernel", False): + from alias_free_activation.cuda.activation1d import ( + Activation1d as CudaActivation1d, + ) + + Activation1d = CudaActivation1d + else: + Activation1d = TorchActivation1d + + # Activation functions + if activation == "snake": + self.activations = nn.ModuleList( + [ + Activation1d( + activation=activations.Snake( + channels, alpha_logscale=h.snake_logscale + ) + ) + for _ in range(self.num_layers) + ] + ) + elif activation == "snakebeta": + 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 BigVGAN( + torch.nn.Module, + PyTorchModelHubMixin, + library_name="bigvgan", + repo_url="https://github.com/NVIDIA/BigVGAN", + docs_url="https://github.com/NVIDIA/BigVGAN/blob/main/README.md", + pipeline_tag="audio-to-audio", + license="mit", + tags=["neural-vocoder", "audio-generation", "arxiv:2206.04658"], +): + """ + BigVGAN is a neural vocoder model that applies anti-aliased periodic activation for residual blocks (resblocks). + New in BigVGAN-v2: it can optionally use optimized CUDA kernels for AMP (anti-aliased multi-periodicity) blocks. + + Args: + h (AttrDict): Hyperparameters. + use_cuda_kernel (bool): If set to True, loads optimized CUDA kernels for AMP. This should be used for inference only, as training is not supported with CUDA kernels. + + Note: + - The `use_cuda_kernel` parameter should be used for inference only, as training with CUDA kernels is not supported. + - Ensure that the activation function is correctly specified in the hyperparameters (h.activation). + """ + + def __init__(self, h: AttrDict, use_cuda_kernel: bool = False): + super().__init__() + self.h = h + self.h["use_cuda_kernel"] = use_cuda_kernel + + # Select which Activation1d, lazy-load cuda version to ensure backward compatibility + if self.h.get("use_cuda_kernel", False): + from alias_free_activation.cuda.activation1d import ( + Activation1d as CudaActivation1d, + ) + + Activation1d = CudaActivation1d + else: + Activation1d = TorchActivation1d + + 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 + if h.resblock == "1": + resblock_class = AMPBlock1 + elif h.resblock == "2": + resblock_class = AMPBlock2 + else: + raise ValueError( + f"Incorrect resblock class specified in hyperparameters. Got {h.resblock}" + ) + + # 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_class(h, ch, k, d, activation=h.activation) + ) + + # Post-conv + activation_post = ( + activations.Snake(ch, alpha_logscale=h.snake_logscale) + if h.activation == "snake" + else ( + activations.SnakeBeta(ch, alpha_logscale=h.snake_logscale) + if h.activation == "snakebeta" + else None + ) + ) + if activation_post is None: + raise NotImplementedError( + "activation incorrectly specified. check the config file and look for 'activation'." + ) + + self.activation_post = Activation1d(activation=activation_post) + + # Whether to use bias for the final conv_post. Default to True for backward compatibility + self.use_bias_at_final = h.get("use_bias_at_final", True) + self.conv_post = weight_norm( + Conv1d(ch, 1, 7, 1, padding=3, bias=self.use_bias_at_final) + ) + + # Weight initialization + for i in range(len(self.ups)): + self.ups[i].apply(init_weights) + self.conv_post.apply(init_weights) + + # Final tanh activation. Defaults to True for backward compatibility + self.use_tanh_at_final = h.get("use_tanh_at_final", True) + + def forward(self, x): + # 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) + # Final tanh activation + if self.use_tanh_at_final: + x = torch.tanh(x) + else: + x = torch.clamp(x, min=-1.0, max=1.0) # Bound the output to [-1, 1] + + return x + + def remove_weight_norm(self): + try: + 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) + except ValueError: + print("[INFO] Model already removed weight norm. Skipping!") + pass + + # Additional methods for huggingface_hub support + def _save_pretrained(self, save_directory: Path) -> None: + """Save weights and config.json from a Pytorch model to a local directory.""" + + model_path = save_directory / "bigvgan_generator.pt" + torch.save({"generator": self.state_dict()}, model_path) + + config_path = save_directory / "config.json" + with open(config_path, "w") as config_file: + json.dump(self.h, config_file, indent=4) + + @classmethod + def _from_pretrained( + cls, + *, + model_id: str, + revision: str, + cache_dir: str, + force_download: bool, + proxies: Optional[Dict], + resume_download: bool, + local_files_only: bool, + token: Union[str, bool, None], + map_location: str = "cpu", # Additional argument + strict: bool = False, # Additional argument + use_cuda_kernel: bool = False, + **model_kwargs, + ): + """Load Pytorch pretrained weights and return the loaded model.""" + + # Download and load hyperparameters (h) used by BigVGAN + if os.path.isdir(model_id): + print("Loading config.json from local directory") + config_file = os.path.join(model_id, "config.json") + else: + config_file = hf_hub_download( + repo_id=model_id, + filename="config.json", + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + token=token, + local_files_only=local_files_only, + ) + h = load_hparams_from_json(config_file) + + # instantiate BigVGAN using h + if use_cuda_kernel: + print( + f"[WARNING] You have specified use_cuda_kernel=True during BigVGAN.from_pretrained(). Only inference is supported (training is not implemented)!" + ) + print( + f"[WARNING] You need nvcc and ninja installed in your system that matches your PyTorch build is using to build the kernel. If not, the model will fail to initialize or generate incorrect waveform!" + ) + print( + f"[WARNING] For detail, see the official GitHub repository: https://github.com/NVIDIA/BigVGAN?tab=readme-ov-file#using-custom-cuda-kernel-for-synthesis" + ) + model = cls(h, use_cuda_kernel=use_cuda_kernel) + + # Download and load pretrained generator weight + if os.path.isdir(model_id): + print("Loading weights from local directory") + model_file = os.path.join(model_id, "bigvgan_generator.pt") + else: + print(f"Loading weights from {model_id}") + model_file = hf_hub_download( + repo_id=model_id, + filename="bigvgan_generator.pt", + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + token=token, + local_files_only=local_files_only, + ) + + checkpoint_dict = torch.load(model_file, map_location=map_location) + + try: + model.load_state_dict(checkpoint_dict["generator"]) + except RuntimeError: + print( + f"[INFO] the pretrained checkpoint does not contain weight norm. Loading the checkpoint after removing weight norm!" + ) + model.remove_weight_norm() + model.load_state_dict(checkpoint_dict["generator"]) + + return model diff --git a/src/third_party/BigVGAN/configs/bigvgan_22khz_80band.json b/src/third_party/BigVGAN/configs/bigvgan_22khz_80band.json new file mode 100644 index 0000000000000000000000000000000000000000..00a0f1f95e10035436a6b8f0130c7289c83b8368 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_22khz_80band.json @@ -0,0 +1,45 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 32, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "activation": "snakebeta", + "snake_logscale": true, + + "resolutions": [[1024, 120, 600], [2048, 240, 1200], [512, 50, 240]], + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "segment_size": 8192, + "num_mels": 80, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 22050, + + "fmin": 0, + "fmax": 8000, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_24khz_100band.json b/src/third_party/BigVGAN/configs/bigvgan_24khz_100band.json new file mode 100644 index 0000000000000000000000000000000000000000..6a2d63c5249236df1318a72b7940eef19b400d90 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_24khz_100band.json @@ -0,0 +1,45 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 32, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "activation": "snakebeta", + "snake_logscale": true, + + "resolutions": [[1024, 120, 600], [2048, 240, 1200], [512, 50, 240]], + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "segment_size": 8192, + "num_mels": 100, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 24000, + + "fmin": 0, + "fmax": 12000, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_base_22khz_80band.json b/src/third_party/BigVGAN/configs/bigvgan_base_22khz_80band.json new file mode 100644 index 0000000000000000000000000000000000000000..df5c56a8dd57f600a03611a8c8044224ad1f43c1 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_base_22khz_80band.json @@ -0,0 +1,45 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 32, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [8,8,2,2], + "upsample_kernel_sizes": [16,16,4,4], + "upsample_initial_channel": 512, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "activation": "snakebeta", + "snake_logscale": true, + + "resolutions": [[1024, 120, 600], [2048, 240, 1200], [512, 50, 240]], + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "segment_size": 8192, + "num_mels": 80, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 22050, + + "fmin": 0, + "fmax": 8000, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_base_24khz_100band.json b/src/third_party/BigVGAN/configs/bigvgan_base_24khz_100band.json new file mode 100644 index 0000000000000000000000000000000000000000..8673a7ed0258629cdb49efc2aad189c6ecd64e1c --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_base_24khz_100band.json @@ -0,0 +1,45 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 32, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [8,8,2,2], + "upsample_kernel_sizes": [16,16,4,4], + "upsample_initial_channel": 512, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "activation": "snakebeta", + "snake_logscale": true, + + "resolutions": [[1024, 120, 600], [2048, 240, 1200], [512, 50, 240]], + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "segment_size": 8192, + "num_mels": 100, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 24000, + + "fmin": 0, + "fmax": 12000, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_256x.json b/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_256x.json new file mode 100644 index 0000000000000000000000000000000000000000..8174afe0eef575d201ce6e1aeab1f94ee1490450 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_256x.json @@ -0,0 +1,61 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 4, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "use_tanh_at_final": false, + "use_bias_at_final": false, + + "activation": "snakebeta", + "snake_logscale": true, + + "use_cqtd_instead_of_mrd": true, + "cqtd_filters": 128, + "cqtd_max_filters": 1024, + "cqtd_filters_scale": 1, + "cqtd_dilations": [1, 2, 4], + "cqtd_hop_lengths": [512, 256, 256], + "cqtd_n_octaves": [9, 9, 9], + "cqtd_bins_per_octaves": [24, 36, 48], + + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "use_multiscale_melloss": true, + "lambda_melloss": 15, + + "clip_grad_norm": 500, + + "segment_size": 65536, + "num_mels": 80, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 22050, + + "fmin": 0, + "fmax": null, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_fmax8k_256x.json b/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_fmax8k_256x.json new file mode 100644 index 0000000000000000000000000000000000000000..f00a0c942326b4153e281c061714c23b8eb9b2d7 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_v2_22khz_80band_fmax8k_256x.json @@ -0,0 +1,61 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 4, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "use_tanh_at_final": false, + "use_bias_at_final": false, + + "activation": "snakebeta", + "snake_logscale": true, + + "use_cqtd_instead_of_mrd": true, + "cqtd_filters": 128, + "cqtd_max_filters": 1024, + "cqtd_filters_scale": 1, + "cqtd_dilations": [1, 2, 4], + "cqtd_hop_lengths": [512, 256, 256], + "cqtd_n_octaves": [9, 9, 9], + "cqtd_bins_per_octaves": [24, 36, 48], + + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "use_multiscale_melloss": true, + "lambda_melloss": 15, + + "clip_grad_norm": 500, + + "segment_size": 65536, + "num_mels": 80, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 22050, + + "fmin": 0, + "fmax": 8000, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_v2_24khz_100band_256x.json b/src/third_party/BigVGAN/configs/bigvgan_v2_24khz_100band_256x.json new file mode 100644 index 0000000000000000000000000000000000000000..072c12d3968baaaa2c49da917a0ded18b22ce648 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_v2_24khz_100band_256x.json @@ -0,0 +1,61 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 4, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "use_tanh_at_final": false, + "use_bias_at_final": false, + + "activation": "snakebeta", + "snake_logscale": true, + + "use_cqtd_instead_of_mrd": true, + "cqtd_filters": 128, + "cqtd_max_filters": 1024, + "cqtd_filters_scale": 1, + "cqtd_dilations": [1, 2, 4], + "cqtd_hop_lengths": [512, 256, 256], + "cqtd_n_octaves": [9, 9, 9], + "cqtd_bins_per_octaves": [24, 36, 48], + + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "use_multiscale_melloss": true, + "lambda_melloss": 15, + + "clip_grad_norm": 500, + + "segment_size": 65536, + "num_mels": 100, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 24000, + + "fmin": 0, + "fmax": null, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_256x.json b/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_256x.json new file mode 100644 index 0000000000000000000000000000000000000000..ea536992e87ee9cd1a7e66847d2e0fd0fe4cf1f5 --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_256x.json @@ -0,0 +1,61 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 4, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [4,4,2,2,2,2], + "upsample_kernel_sizes": [8,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "use_tanh_at_final": false, + "use_bias_at_final": false, + + "activation": "snakebeta", + "snake_logscale": true, + + "use_cqtd_instead_of_mrd": true, + "cqtd_filters": 128, + "cqtd_max_filters": 1024, + "cqtd_filters_scale": 1, + "cqtd_dilations": [1, 2, 4], + "cqtd_hop_lengths": [512, 256, 256], + "cqtd_n_octaves": [9, 9, 9], + "cqtd_bins_per_octaves": [24, 36, 48], + + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "use_multiscale_melloss": true, + "lambda_melloss": 15, + + "clip_grad_norm": 500, + + "segment_size": 65536, + "num_mels": 128, + "num_freq": 1025, + "n_fft": 1024, + "hop_size": 256, + "win_size": 1024, + + "sampling_rate": 44100, + + "fmin": 0, + "fmax": null, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_512x.json b/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_512x.json new file mode 100644 index 0000000000000000000000000000000000000000..866e130b3c26902e9931e0fe8bd74602ffc2cdbf --- /dev/null +++ b/src/third_party/BigVGAN/configs/bigvgan_v2_44khz_128band_512x.json @@ -0,0 +1,61 @@ +{ + "resblock": "1", + "num_gpus": 0, + "batch_size": 4, + "learning_rate": 0.0001, + "adam_b1": 0.8, + "adam_b2": 0.99, + "lr_decay": 0.9999996, + "seed": 1234, + + "upsample_rates": [8,4,2,2,2,2], + "upsample_kernel_sizes": [16,8,4,4,4,4], + "upsample_initial_channel": 1536, + "resblock_kernel_sizes": [3,7,11], + "resblock_dilation_sizes": [[1,3,5], [1,3,5], [1,3,5]], + + "use_tanh_at_final": false, + "use_bias_at_final": false, + + "activation": "snakebeta", + "snake_logscale": true, + + "use_cqtd_instead_of_mrd": true, + "cqtd_filters": 128, + "cqtd_max_filters": 1024, + "cqtd_filters_scale": 1, + "cqtd_dilations": [1, 2, 4], + "cqtd_hop_lengths": [512, 256, 256], + "cqtd_n_octaves": [9, 9, 9], + "cqtd_bins_per_octaves": [24, 36, 48], + + "mpd_reshapes": [2, 3, 5, 7, 11], + "use_spectral_norm": false, + "discriminator_channel_mult": 1, + + "use_multiscale_melloss": true, + "lambda_melloss": 15, + + "clip_grad_norm": 500, + + "segment_size": 65536, + "num_mels": 128, + "num_freq": 2049, + "n_fft": 2048, + "hop_size": 512, + "win_size": 2048, + + "sampling_rate": 44100, + + "fmin": 0, + "fmax": null, + "fmax_for_loss": null, + + "num_workers": 4, + + "dist_config": { + "dist_backend": "nccl", + "dist_url": "tcp://localhost:54321", + "world_size": 1 + } +} diff --git a/src/third_party/BigVGAN/demo/__init__.py b/src/third_party/BigVGAN/demo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/third_party/BigVGAN/demo/app.py b/src/third_party/BigVGAN/demo/app.py new file mode 100644 index 0000000000000000000000000000000000000000..f1436f8ea57758660a87cb8c22f0fbde3fbc7127 --- /dev/null +++ b/src/third_party/BigVGAN/demo/app.py @@ -0,0 +1,441 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import spaces +import gradio as gr +import pandas as pd +import torch +import os +import sys + +# to import modules from parent_dir +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) + +from meldataset import get_mel_spectrogram, MAX_WAV_VALUE +from bigvgan import BigVGAN +import librosa +import numpy as np +from utils import plot_spectrogram +import PIL + +if torch.cuda.is_available(): + device = torch.device("cuda") + torch.backends.cudnn.benchmark = False + print(f"using GPU") +else: + device = torch.device("cpu") + print(f"using CPU") + + +def inference_gradio(input, model_choice): # Input is audio waveform in [T, channel] + sr, audio = input # Unpack input to sampling rate and audio itself + audio = np.transpose(audio) # Transpose to [channel, T] for librosa + audio = audio / MAX_WAV_VALUE # Convert int16 to float range used by BigVGAN + + model = dict_model[model_choice] + + if sr != model.h.sampling_rate: # Convert audio to model's sampling rate + audio = librosa.resample(audio, orig_sr=sr, target_sr=model.h.sampling_rate) + if len(audio.shape) == 2: # Stereo + audio = librosa.to_mono(audio) # Convert to mono if stereo + audio = librosa.util.normalize(audio) * 0.95 + + output, spec_gen = inference_model( + audio, model + ) # Output is generated audio in ndarray, int16 + + spec_plot_gen = plot_spectrogram(spec_gen) + + output_audio = (model.h.sampling_rate, output) # Tuple for gr.Audio output + + buffer = spec_plot_gen.canvas.buffer_rgba() + output_image = PIL.Image.frombuffer( + "RGBA", spec_plot_gen.canvas.get_width_height(), buffer, "raw", "RGBA", 0, 1 + ) + + return output_audio, output_image + + +@spaces.GPU(duration=120) +def inference_model(audio_input, model): + # Load model to device + model.to(device) + + with torch.inference_mode(): + wav = torch.FloatTensor(audio_input) + # Compute mel spectrogram from the ground truth audio + spec_gt = get_mel_spectrogram(wav.unsqueeze(0), model.h).to(device) + + y_g_hat = model(spec_gt) + + audio_gen = y_g_hat.squeeze().cpu() + spec_gen = get_mel_spectrogram(audio_gen.unsqueeze(0), model.h) + audio_gen = audio_gen.numpy() # [T], float [-1, 1] + audio_gen = (audio_gen * MAX_WAV_VALUE).astype("int16") # [T], int16 + spec_gen = spec_gen.squeeze().numpy() # [C, T_frame] + + # Unload to CPU + model.to("cpu") + # Delete GPU tensor + del spec_gt, y_g_hat + + return audio_gen, spec_gen + + +css = """ + a { + color: inherit; + text-decoration: underline; + } + .gradio-container { + font-family: 'IBM Plex Sans', sans-serif; + } + .gr-button { + color: white; + border-color: #000000; + background: #000000; + } + input[type='range'] { + accent-color: #000000; + } + .dark input[type='range'] { + accent-color: #dfdfdf; + } + .container { + max-width: 730px; + margin: auto; + padding-top: 1.5rem; + } + #gallery { + min-height: 22rem; + margin-bottom: 15px; + margin-left: auto; + margin-right: auto; + border-bottom-right-radius: .5rem !important; + border-bottom-left-radius: .5rem !important; + } + #gallery>div>.h-full { + min-height: 20rem; + } + .details:hover { + text-decoration: underline; + } + .gr-button { + white-space: nowrap; + } + .gr-button:focus { + border-color: rgb(147 197 253 / var(--tw-border-opacity)); + outline: none; + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-border-opacity: 1; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px var(--tw-ring-offset-width)) var(--tw-ring-color); + --tw-ring-color: rgb(191 219 254 / var(--tw-ring-opacity)); + --tw-ring-opacity: .5; + } + #advanced-btn { + font-size: .7rem !important; + line-height: 19px; + margin-top: 12px; + margin-bottom: 12px; + padding: 2px 8px; + border-radius: 14px !important; + } + #advanced-options { + margin-bottom: 20px; + } + .footer { + margin-bottom: 45px; + margin-top: 35px; + text-align: center; + border-bottom: 1px solid #e5e5e5; + } + .footer>p { + font-size: .8rem; + display: inline-block; + padding: 0 10px; + transform: translateY(10px); + background: white; + } + .dark .footer { + border-color: #303030; + } + .dark .footer>p { + background: #0b0f19; + } + .acknowledgments h4{ + margin: 1.25em 0 .25em 0; + font-weight: bold; + font-size: 115%; + } + #container-advanced-btns{ + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + } + .animate-spin { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + #share-btn-container { + display: flex; padding-left: 0.5rem !important; padding-right: 0.5rem !important; background-color: #000000; justify-content: center; align-items: center; border-radius: 9999px !important; width: 13rem; + margin-top: 10px; + margin-left: auto; + } + #share-btn { + all: initial; color: #ffffff;font-weight: 600; cursor:pointer; font-family: 'IBM Plex Sans', sans-serif; margin-left: 0.5rem !important; padding-top: 0.25rem !important; padding-bottom: 0.25rem !important;right:0; + } + #share-btn * { + all: unset; + } + #share-btn-container div:nth-child(-n+2){ + width: auto !important; + min-height: 0px !important; + } + #share-btn-container .wrap { + display: none !important; + } + .gr-form{ + flex: 1 1 50%; border-top-right-radius: 0; border-bottom-right-radius: 0; + } + #prompt-container{ + gap: 0; + } + #generated_id{ + min-height: 700px + } + #setting_id{ + margin-bottom: 12px; + text-align: center; + font-weight: 900; + } +""" + +# Script for loading the models + +LIST_MODEL_ID = [ + "bigvgan_24khz_100band", + "bigvgan_base_24khz_100band", + "bigvgan_22khz_80band", + "bigvgan_base_22khz_80band", + "bigvgan_v2_22khz_80band_256x", + "bigvgan_v2_22khz_80band_fmax8k_256x", + "bigvgan_v2_24khz_100band_256x", + "bigvgan_v2_44khz_128band_256x", + "bigvgan_v2_44khz_128band_512x", +] + +dict_model = {} +dict_config = {} + +for model_name in LIST_MODEL_ID: + + generator = BigVGAN.from_pretrained("nvidia/" + model_name) + generator.remove_weight_norm() + generator.eval() + + dict_model[model_name] = generator + dict_config[model_name] = generator.h + +# Script for Gradio UI + +iface = gr.Blocks(css=css, title="BigVGAN - Demo") + +with iface: + gr.HTML( + """ +
+
+

+ BigVGAN: A Universal Neural Vocoder with Large-Scale Training +

+
+

+ [Paper] [Code] [Demo] [Project page] +

+
+ """ + ) + gr.HTML( + """ +
+

News

+

[Jul 2024] We release BigVGAN-v2 along with pretrained checkpoints. Below are the highlights:

+
    +
  • Custom CUDA kernel for inference: we provide a fused upsampling + activation kernel written in CUDA for accelerated inference speed. Our test shows 1.5 - 3x faster speed on a single A100 GPU.
  • +
  • Improved discriminator and loss: BigVGAN-v2 is trained using a multi-scale sub-band CQT discriminator and a multi-scale mel spectrogram loss.
  • +
  • Larger training data: BigVGAN-v2 is trained using datasets containing diverse audio types, including speech in multiple languages, environmental sounds, and instruments.
  • +
  • We provide pretrained checkpoints of BigVGAN-v2 using diverse audio configurations, supporting up to 44 kHz sampling rate and 512x upsampling ratio. See the table below for the link.
  • +
+
+ """ + ) + gr.HTML( + """ +
+

Model Overview

+ BigVGAN is a universal neural vocoder model that generates audio waveforms using mel spectrogram as inputs. +
+
+ """ + ) + with gr.Accordion("Input"): + + model_choice = gr.Dropdown( + label="Select the model to use", + info="The default model is bigvgan_v2_24khz_100band_256x", + value="bigvgan_v2_24khz_100band_256x", + choices=[m for m in LIST_MODEL_ID], + interactive=True, + ) + + audio_input = gr.Audio( + label="Input Audio", elem_id="input-audio", interactive=True + ) + + button = gr.Button("Submit") + + with gr.Accordion("Output"): + with gr.Column(): + output_audio = gr.Audio(label="Output Audio", elem_id="output-audio") + output_image = gr.Image( + label="Output Mel Spectrogram", elem_id="output-image-gen" + ) + + button.click( + inference_gradio, + inputs=[audio_input, model_choice], + outputs=[output_audio, output_image], + concurrency_limit=10, + ) + + gr.Examples( + [ + [ + os.path.join(os.path.dirname(__file__), "examples/jensen_24k.wav"), + "bigvgan_v2_24khz_100band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/libritts_24k.wav"), + "bigvgan_v2_24khz_100band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/queen_24k.wav"), + "bigvgan_v2_24khz_100band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/dance_24k.wav"), + "bigvgan_v2_24khz_100band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/megalovania_24k.wav"), + "bigvgan_v2_24khz_100band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/hifitts_44k.wav"), + "bigvgan_v2_44khz_128band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/musdbhq_44k.wav"), + "bigvgan_v2_44khz_128band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/musiccaps1_44k.wav"), + "bigvgan_v2_44khz_128band_256x", + ], + [ + os.path.join(os.path.dirname(__file__), "examples/musiccaps2_44k.wav"), + "bigvgan_v2_44khz_128band_256x", + ], + ], + fn=inference_gradio, + inputs=[audio_input, model_choice], + outputs=[output_audio, output_image], + ) + + # Define the data for the table + data = { + "Model Name": [ + "bigvgan_v2_44khz_128band_512x", + "bigvgan_v2_44khz_128band_256x", + "bigvgan_v2_24khz_100band_256x", + "bigvgan_v2_22khz_80band_256x", + "bigvgan_v2_22khz_80band_fmax8k_256x", + "bigvgan_24khz_100band", + "bigvgan_base_24khz_100band", + "bigvgan_22khz_80band", + "bigvgan_base_22khz_80band", + ], + "Sampling Rate": [ + "44 kHz", + "44 kHz", + "24 kHz", + "22 kHz", + "22 kHz", + "24 kHz", + "24 kHz", + "22 kHz", + "22 kHz", + ], + "Mel band": [128, 128, 100, 80, 80, 100, 100, 80, 80], + "fmax": [22050, 22050, 12000, 11025, 8000, 12000, 12000, 8000, 8000], + "Upsampling Ratio": [512, 256, 256, 256, 256, 256, 256, 256, 256], + "Parameters": [ + "122M", + "112M", + "112M", + "112M", + "112M", + "112M", + "14M", + "112M", + "14M", + ], + "Dataset": [ + "Large-scale Compilation", + "Large-scale Compilation", + "Large-scale Compilation", + "Large-scale Compilation", + "Large-scale Compilation", + "LibriTTS", + "LibriTTS", + "LibriTTS + VCTK + LJSpeech", + "LibriTTS + VCTK + LJSpeech", + ], + "Fine-Tuned": ["No", "No", "No", "No", "No", "No", "No", "No", "No"], + } + + base_url = "https://huggingface.co/nvidia/" + + df = pd.DataFrame(data) + df["Model Name"] = df["Model Name"].apply( + lambda x: f'{x}' + ) + + html_table = gr.HTML( + f""" +
+ {df.to_html(index=False, escape=False, classes='border="1" cellspacing="0" cellpadding="5" style="margin-left: auto; margin-right: auto;')} +

NOTE: The v1 models are trained using speech audio datasets ONLY! (24kHz models: LibriTTS, 22kHz models: LibriTTS + VCTK + LJSpeech).

+
+ """ + ) + +iface.queue() +iface.launch() diff --git a/src/third_party/BigVGAN/demo/examples/dance_24k.wav b/src/third_party/BigVGAN/demo/examples/dance_24k.wav new file mode 100644 index 0000000000000000000000000000000000000000..6d996759b067c5f2b00ef8925b2f8c3fb4bedc71 Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/dance_24k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/hifitts_44k.wav b/src/third_party/BigVGAN/demo/examples/hifitts_44k.wav new file mode 100644 index 0000000000000000000000000000000000000000..b5823928e5a884c5f86e706fc94629ae65d51fa8 Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/hifitts_44k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/jensen_24k.wav b/src/third_party/BigVGAN/demo/examples/jensen_24k.wav new file mode 100644 index 0000000000000000000000000000000000000000..0096987ad94072e0108b1a420bb8892fb6a2d7ac Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/jensen_24k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/libritts_24k.wav b/src/third_party/BigVGAN/demo/examples/libritts_24k.wav new file mode 100644 index 0000000000000000000000000000000000000000..54fb14a947b241137ec000a97be85569eb4c9bc8 Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/libritts_24k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/megalovania_24k.wav b/src/third_party/BigVGAN/demo/examples/megalovania_24k.wav new file mode 100644 index 0000000000000000000000000000000000000000..593127d88b4d829a4fd91ede0972db20eac86766 --- /dev/null +++ b/src/third_party/BigVGAN/demo/examples/megalovania_24k.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7970ac637e680876d48ad84e9185db1b21da01929fe46d855e8794bd83d14c20 +size 1548328 diff --git a/src/third_party/BigVGAN/demo/examples/musdbhq_44k.wav b/src/third_party/BigVGAN/demo/examples/musdbhq_44k.wav new file mode 100644 index 0000000000000000000000000000000000000000..77176040e051a5063e9175b88582fc1fbea614a5 Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/musdbhq_44k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/musiccaps1_44k.wav b/src/third_party/BigVGAN/demo/examples/musiccaps1_44k.wav new file mode 100644 index 0000000000000000000000000000000000000000..50a321456021a712ebd43155577785b76038d393 Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/musiccaps1_44k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/musiccaps2_44k.wav b/src/third_party/BigVGAN/demo/examples/musiccaps2_44k.wav new file mode 100644 index 0000000000000000000000000000000000000000..debb5cabe73dc408d247e588e957085ad10b653e Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/musiccaps2_44k.wav differ diff --git a/src/third_party/BigVGAN/demo/examples/queen_24k.wav b/src/third_party/BigVGAN/demo/examples/queen_24k.wav new file mode 100644 index 0000000000000000000000000000000000000000..e40af250f95d4ed6dbf37a2aa5018e1c4ead044b Binary files /dev/null and b/src/third_party/BigVGAN/demo/examples/queen_24k.wav differ diff --git a/src/third_party/BigVGAN/demo/requirements.txt b/src/third_party/BigVGAN/demo/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f189bcd63b8259be23d163e3848b585406059688 --- /dev/null +++ b/src/third_party/BigVGAN/demo/requirements.txt @@ -0,0 +1,15 @@ +torch +numpy +librosa>=0.8.1 +scipy +tensorboard +soundfile +matplotlib +pesq +auraloss +tqdm +nnAudio +ninja +huggingface_hub>=0.23.4 +gradio>=4.38.1 +spaces>=0.28.3 \ No newline at end of file diff --git a/src/third_party/BigVGAN/discriminators.py b/src/third_party/BigVGAN/discriminators.py new file mode 100644 index 0000000000000000000000000000000000000000..197f6ff77e8040e1aad8ce1ffe7c871653dbb0d8 --- /dev/null +++ b/src/third_party/BigVGAN/discriminators.py @@ -0,0 +1,651 @@ +# Copyright (c) 2024 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 torch +import torch.nn.functional as F +import torch.nn as nn +from torch.nn import Conv2d +from torch.nn.utils import weight_norm, spectral_norm +from torchaudio.transforms import Spectrogram, Resample + +from env import AttrDict +from utils import get_padding +import typing +from typing import Optional, List, Union, Dict, Tuple + + +class DiscriminatorP(torch.nn.Module): + def __init__( + self, + h: AttrDict, + period: List[int], + kernel_size: int = 5, + stride: int = 3, + use_spectral_norm: bool = False, + ): + super().__init__() + self.period = period + self.d_mult = h.discriminator_channel_mult + norm_f = weight_norm if not use_spectral_norm 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: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]: + 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, 0.1) + fmap.append(x) + x = self.conv_post(x) + fmap.append(x) + x = torch.flatten(x, 1, -1) + + return x, fmap + + +class MultiPeriodDiscriminator(torch.nn.Module): + def __init__(self, h: AttrDict): + super().__init__() + self.mpd_reshapes = h.mpd_reshapes + print(f"mpd_reshapes: {self.mpd_reshapes}") + self.discriminators = nn.ModuleList( + [ + DiscriminatorP(h, rs, use_spectral_norm=h.use_spectral_norm) + for rs in self.mpd_reshapes + ] + ) + + def forward(self, y: torch.Tensor, y_hat: torch.Tensor) -> Tuple[ + List[torch.Tensor], + List[torch.Tensor], + List[List[torch.Tensor]], + List[List[torch.Tensor]], + ]: + 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: AttrDict, resolution: List[List[int]]): + super().__init__() + + self.resolution = resolution + assert ( + len(self.resolution) == 3 + ), f"MRD layer requires list with len=3, got {self.resolution}" + self.lrelu_slope = 0.1 + + norm_f = weight_norm if cfg.use_spectral_norm == False else spectral_norm + if hasattr(cfg, "mrd_use_spectral_norm"): + print( + f"[INFO] overriding MRD use_spectral_norm as {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(f"[INFO] overriding mrd channel multiplier as {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: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]: + 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: torch.Tensor) -> torch.Tensor: + 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 + ), f"MRD requires list of list with len=3, each element having a list with len=3. Got {self.resolutions}" + self.discriminators = nn.ModuleList( + [DiscriminatorR(cfg, resolution) for resolution in self.resolutions] + ) + + def forward(self, y: torch.Tensor, y_hat: torch.Tensor) -> Tuple[ + List[torch.Tensor], + List[torch.Tensor], + List[List[torch.Tensor]], + List[List[torch.Tensor]], + ]: + 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 + + +# Method based on descript-audio-codec: https://github.com/descriptinc/descript-audio-codec +# Modified code adapted from https://github.com/gemelo-ai/vocos under the MIT license. +# LICENSE is in incl_licenses directory. +class DiscriminatorB(nn.Module): + def __init__( + self, + window_length: int, + channels: int = 32, + hop_factor: float = 0.25, + bands: Tuple[Tuple[float, float], ...] = ( + (0.0, 0.1), + (0.1, 0.25), + (0.25, 0.5), + (0.5, 0.75), + (0.75, 1.0), + ), + ): + super().__init__() + self.window_length = window_length + self.hop_factor = hop_factor + self.spec_fn = Spectrogram( + n_fft=window_length, + hop_length=int(window_length * hop_factor), + win_length=window_length, + power=None, + ) + n_fft = window_length // 2 + 1 + bands = [(int(b[0] * n_fft), int(b[1] * n_fft)) for b in bands] + self.bands = bands + convs = lambda: nn.ModuleList( + [ + weight_norm(nn.Conv2d(2, channels, (3, 9), (1, 1), padding=(1, 4))), + weight_norm( + nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4)) + ), + weight_norm( + nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4)) + ), + weight_norm( + nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4)) + ), + weight_norm( + nn.Conv2d(channels, channels, (3, 3), (1, 1), padding=(1, 1)) + ), + ] + ) + self.band_convs = nn.ModuleList([convs() for _ in range(len(self.bands))]) + + self.conv_post = weight_norm( + nn.Conv2d(channels, 1, (3, 3), (1, 1), padding=(1, 1)) + ) + + def spectrogram(self, x: torch.Tensor) -> List[torch.Tensor]: + # Remove DC offset + x = x - x.mean(dim=-1, keepdims=True) + # Peak normalize the volume of input audio + x = 0.8 * x / (x.abs().max(dim=-1, keepdim=True)[0] + 1e-9) + x = self.spec_fn(x) + x = torch.view_as_real(x) + x = x.permute(0, 3, 2, 1) # [B, F, T, C] -> [B, C, T, F] + # Split into bands + x_bands = [x[..., b[0] : b[1]] for b in self.bands] + return x_bands + + def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]: + x_bands = self.spectrogram(x.squeeze(1)) + fmap = [] + x = [] + + for band, stack in zip(x_bands, self.band_convs): + for i, layer in enumerate(stack): + band = layer(band) + band = torch.nn.functional.leaky_relu(band, 0.1) + if i > 0: + fmap.append(band) + x.append(band) + + x = torch.cat(x, dim=-1) + x = self.conv_post(x) + fmap.append(x) + + return x, fmap + + +# Method based on descript-audio-codec: https://github.com/descriptinc/descript-audio-codec +# Modified code adapted from https://github.com/gemelo-ai/vocos under the MIT license. +# LICENSE is in incl_licenses directory. +class MultiBandDiscriminator(nn.Module): + def __init__( + self, + h, + ): + """ + Multi-band multi-scale STFT discriminator, with the architecture based on https://github.com/descriptinc/descript-audio-codec. + and the modified code adapted from https://github.com/gemelo-ai/vocos. + """ + super().__init__() + # fft_sizes (list[int]): Tuple of window lengths for FFT. Defaults to [2048, 1024, 512] if not set in h. + self.fft_sizes = h.get("mbd_fft_sizes", [2048, 1024, 512]) + self.discriminators = nn.ModuleList( + [DiscriminatorB(window_length=w) for w in self.fft_sizes] + ) + + def forward(self, y: torch.Tensor, y_hat: torch.Tensor) -> Tuple[ + List[torch.Tensor], + List[torch.Tensor], + List[List[torch.Tensor]], + List[List[torch.Tensor]], + ]: + + y_d_rs = [] + y_d_gs = [] + fmap_rs = [] + fmap_gs = [] + + for d in 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 + + +# Adapted from https://github.com/open-mmlab/Amphion/blob/main/models/vocoders/gan/discriminator/mssbcqtd.py under the MIT license. +# LICENSE is in incl_licenses directory. +class DiscriminatorCQT(nn.Module): + def __init__(self, cfg: AttrDict, hop_length: int, n_octaves:int, bins_per_octave: int): + super().__init__() + self.cfg = cfg + + self.filters = cfg["cqtd_filters"] + self.max_filters = cfg["cqtd_max_filters"] + self.filters_scale = cfg["cqtd_filters_scale"] + self.kernel_size = (3, 9) + self.dilations = cfg["cqtd_dilations"] + self.stride = (1, 2) + + self.in_channels = cfg["cqtd_in_channels"] + self.out_channels = cfg["cqtd_out_channels"] + self.fs = cfg["sampling_rate"] + self.hop_length = hop_length + self.n_octaves = n_octaves + self.bins_per_octave = bins_per_octave + + # Lazy-load + from nnAudio import features + + self.cqt_transform = features.cqt.CQT2010v2( + sr=self.fs * 2, + hop_length=self.hop_length, + n_bins=self.bins_per_octave * self.n_octaves, + bins_per_octave=self.bins_per_octave, + output_format="Complex", + pad_mode="constant", + ) + + self.conv_pres = nn.ModuleList() + for _ in range(self.n_octaves): + self.conv_pres.append( + nn.Conv2d( + self.in_channels * 2, + self.in_channels * 2, + kernel_size=self.kernel_size, + padding=self.get_2d_padding(self.kernel_size), + ) + ) + + self.convs = nn.ModuleList() + + self.convs.append( + nn.Conv2d( + self.in_channels * 2, + self.filters, + kernel_size=self.kernel_size, + padding=self.get_2d_padding(self.kernel_size), + ) + ) + + in_chs = min(self.filters_scale * self.filters, self.max_filters) + for i, dilation in enumerate(self.dilations): + out_chs = min( + (self.filters_scale ** (i + 1)) * self.filters, self.max_filters + ) + self.convs.append( + weight_norm( + nn.Conv2d( + in_chs, + out_chs, + kernel_size=self.kernel_size, + stride=self.stride, + dilation=(dilation, 1), + padding=self.get_2d_padding(self.kernel_size, (dilation, 1)), + ) + ) + ) + in_chs = out_chs + out_chs = min( + (self.filters_scale ** (len(self.dilations) + 1)) * self.filters, + self.max_filters, + ) + self.convs.append( + weight_norm( + nn.Conv2d( + in_chs, + out_chs, + kernel_size=(self.kernel_size[0], self.kernel_size[0]), + padding=self.get_2d_padding( + (self.kernel_size[0], self.kernel_size[0]) + ), + ) + ) + ) + + self.conv_post = weight_norm( + nn.Conv2d( + out_chs, + self.out_channels, + kernel_size=(self.kernel_size[0], self.kernel_size[0]), + padding=self.get_2d_padding((self.kernel_size[0], self.kernel_size[0])), + ) + ) + + self.activation = torch.nn.LeakyReLU(negative_slope=0.1) + self.resample = Resample(orig_freq=self.fs, new_freq=self.fs * 2) + + self.cqtd_normalize_volume = self.cfg.get("cqtd_normalize_volume", False) + if self.cqtd_normalize_volume: + print( + f"[INFO] cqtd_normalize_volume set to True. Will apply DC offset removal & peak volume normalization in CQTD!" + ) + + def get_2d_padding( + self, + kernel_size: typing.Tuple[int, int], + dilation: typing.Tuple[int, int] = (1, 1), + ): + return ( + ((kernel_size[0] - 1) * dilation[0]) // 2, + ((kernel_size[1] - 1) * dilation[1]) // 2, + ) + + def forward(self, x: torch.tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]: + fmap = [] + + if self.cqtd_normalize_volume: + # Remove DC offset + x = x - x.mean(dim=-1, keepdims=True) + # Peak normalize the volume of input audio + x = 0.8 * x / (x.abs().max(dim=-1, keepdim=True)[0] + 1e-9) + + x = self.resample(x) + + z = self.cqt_transform(x) + + z_amplitude = z[:, :, :, 0].unsqueeze(1) + z_phase = z[:, :, :, 1].unsqueeze(1) + + z = torch.cat([z_amplitude, z_phase], dim=1) + z = torch.permute(z, (0, 1, 3, 2)) # [B, C, W, T] -> [B, C, T, W] + + latent_z = [] + for i in range(self.n_octaves): + latent_z.append( + self.conv_pres[i]( + z[ + :, + :, + :, + i * self.bins_per_octave : (i + 1) * self.bins_per_octave, + ] + ) + ) + latent_z = torch.cat(latent_z, dim=-1) + + for i, l in enumerate(self.convs): + latent_z = l(latent_z) + + latent_z = self.activation(latent_z) + fmap.append(latent_z) + + latent_z = self.conv_post(latent_z) + + return latent_z, fmap + + +class MultiScaleSubbandCQTDiscriminator(nn.Module): + def __init__(self, cfg: AttrDict): + super().__init__() + + self.cfg = cfg + # Using get with defaults + self.cfg["cqtd_filters"] = self.cfg.get("cqtd_filters", 32) + self.cfg["cqtd_max_filters"] = self.cfg.get("cqtd_max_filters", 1024) + self.cfg["cqtd_filters_scale"] = self.cfg.get("cqtd_filters_scale", 1) + self.cfg["cqtd_dilations"] = self.cfg.get("cqtd_dilations", [1, 2, 4]) + self.cfg["cqtd_in_channels"] = self.cfg.get("cqtd_in_channels", 1) + self.cfg["cqtd_out_channels"] = self.cfg.get("cqtd_out_channels", 1) + # Multi-scale params to loop over + self.cfg["cqtd_hop_lengths"] = self.cfg.get("cqtd_hop_lengths", [512, 256, 256]) + self.cfg["cqtd_n_octaves"] = self.cfg.get("cqtd_n_octaves", [9, 9, 9]) + self.cfg["cqtd_bins_per_octaves"] = self.cfg.get( + "cqtd_bins_per_octaves", [24, 36, 48] + ) + + self.discriminators = nn.ModuleList( + [ + DiscriminatorCQT( + self.cfg, + hop_length=self.cfg["cqtd_hop_lengths"][i], + n_octaves=self.cfg["cqtd_n_octaves"][i], + bins_per_octave=self.cfg["cqtd_bins_per_octaves"][i], + ) + for i in range(len(self.cfg["cqtd_hop_lengths"])) + ] + ) + + def forward(self, y: torch.Tensor, y_hat: torch.Tensor) -> Tuple[ + List[torch.Tensor], + List[torch.Tensor], + List[List[torch.Tensor]], + List[List[torch.Tensor]], + ]: + + y_d_rs = [] + y_d_gs = [] + fmap_rs = [] + fmap_gs = [] + + for disc in self.discriminators: + y_d_r, fmap_r = disc(y) + y_d_g, fmap_g = disc(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 CombinedDiscriminator(nn.Module): + """ + Wrapper of chaining multiple discrimiantor architectures. + Example: combine mbd and cqtd as a single class + """ + + def __init__(self, list_discriminator: List[nn.Module]): + super().__init__() + self.discrimiantor = nn.ModuleList(list_discriminator) + + def forward(self, y: torch.Tensor, y_hat: torch.Tensor) -> Tuple[ + List[torch.Tensor], + List[torch.Tensor], + List[List[torch.Tensor]], + List[List[torch.Tensor]], + ]: + + y_d_rs = [] + y_d_gs = [] + fmap_rs = [] + fmap_gs = [] + + for disc in self.discrimiantor: + y_d_r, y_d_g, fmap_r, fmap_g = disc(y, y_hat) + y_d_rs.extend(y_d_r) + fmap_rs.extend(fmap_r) + y_d_gs.extend(y_d_g) + fmap_gs.extend(fmap_g) + + return y_d_rs, y_d_gs, fmap_rs, fmap_gs diff --git a/src/third_party/BigVGAN/env.py b/src/third_party/BigVGAN/env.py new file mode 100644 index 0000000000000000000000000000000000000000..ebc6c9a6b460f13b59761bebf69c43bd6a6ecf1d --- /dev/null +++ b/src/third_party/BigVGAN/env.py @@ -0,0 +1,18 @@ +# Adapted from https://github.com/jik876/hifi-gan under the MIT license. +# LICENSE is in incl_licenses directory. + +import os +import shutil + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + +def build_env(config, config_name, path): + t_path = os.path.join(path, config_name) + if config != t_path: + os.makedirs(path, exist_ok=True) + shutil.copyfile(config, os.path.join(path, config_name)) diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/dev-clean.txt b/src/third_party/BigVGAN/filelists/LibriTTS/dev-clean.txt new file mode 100644 index 0000000000000000000000000000000000000000..0566702740c44e766bdfa6f39a15bdb6953d5bdc --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/dev-clean.txt @@ -0,0 +1,115 @@ +dev-clean/1272/128104/1272_128104_000001_000000|A 'JOLLY' ART CRITIC +dev-clean/1272/141231/1272_141231_000007_000003|And when he attacked, it was always there to beat him aside. +dev-clean/1272/141231/1272_141231_000033_000002|If anything, he was pressing the attack. +dev-clean/1462/170138/1462_170138_000012_000002|Dear me, Mac, the girl couldn't possibly be better, you know." +dev-clean/1462/170142/1462_170142_000002_000005|Alexander did not sit down. +dev-clean/1462/170142/1462_170142_000029_000001|"I meant to, but somehow I couldn't. +dev-clean/1462/170142/1462_170142_000046_000004|The sight of you, Bartley, to see you living and happy and successful-can I never make you understand what that means to me?" She pressed his shoulders gently. +dev-clean/1462/170145/1462_170145_000012_000003|There is a letter for you there, in my desk drawer. +dev-clean/1462/170145/1462_170145_000033_000000|She felt the strength leap in the arms that held her so lightly. +dev-clean/1673/143397/1673_143397_000031_000007|He attempted to remove or intimidate the leaders by a common sentence, of acquittal or condemnation; he invested his representatives at Ephesus with ample power and military force; he summoned from either party eight chosen deputies to a free and candid conference in the neighborhood of the capital, far from the contagion of popular frenzy. +dev-clean/174/168635/174_168635_000040_000000|To teach Cosette to read, and to let her play, this constituted nearly the whole of Jean Valjean's existence. +dev-clean/174/50561/174_50561_000058_000001|They have the end of the game to themselves.) +dev-clean/174/84280/174_84280_000015_000000|And perhaps in this story I have said enough for you to understand why Mary has identified herself with something world-wide, has added to herself a symbolical value, and why it is I find in the whole crowded spectacle of mankind, a quality that is also hers, a sense of fine things entangled and stifled and unable to free themselves from the ancient limiting jealousies which law and custom embody. +dev-clean/1919/142785/1919_142785_000063_000000|[Illustration: SHALOT.] +dev-clean/1919/142785/1919_142785_000131_000001|Cut the bread into thin slices, place them in a cool oven overnight, and when thoroughly dry and crisp, roll them down into fine crumbs. +dev-clean/1988/147956/1988_147956_000016_000009|He was neatly dressed. +dev-clean/1988/148538/1988_148538_000015_000007|These persons then displayed towards each other precisely the same puerile jealousies which animate the men of democracies, the same eagerness to snatch the smallest advantages which their equals contested, and the same desire to parade ostentatiously those of which they were in possession. +dev-clean/1988/24833/1988_24833_000028_000003|He's taking the kid for a walk when a thunderstorm blows up. +dev-clean/1988/24833/1988_24833_000059_000000|"Doesn't pay enough?" Pop asks. +dev-clean/1993/147149/1993_147149_000051_000002|So leaving kind messages to George and Jane Wilson, and hesitating whether she might dare to send a few kind words to Jem, and deciding that she had better not, she stepped out into the bright morning light, so fresh a contrast to the darkened room where death had been. +dev-clean/1993/147965/1993_147965_000003_000004|I suppose, in the crowded clutter of their cave, the old man had come to believe that peace and order had vanished from the earth, or existed only in the old world he had left so far behind. +dev-clean/1993/147966/1993_147966_000020_000003|We found the chickens asleep; perhaps they thought night had come to stay. +dev-clean/2035/147960/2035_147960_000019_000001|He is all over Jimmy's boots. I scream for him to run, but he just hit and hit that snake like he was crazy." +dev-clean/2035/147961/2035_147961_000011_000002|He grew more and more excited, and kept pointing all around his bed, as if there were things there and he wanted mr Shimerda to see them. +dev-clean/2035/147961/2035_147961_000025_000002|Beside a frozen pond something happened to the other sledge; peter saw it plainly. +dev-clean/2035/152373/2035_152373_000010_000007|saint Aidan, the Apostle of Northumbria, had refused the late Egfrid's father absolution, on one occasion, until he solemnly promised to restore their freedom to certain captives of this description. +dev-clean/2086/149214/2086_149214_000005_000002|It is a legend prolonging itself, from an epoch now gray in the distance, down into our own broad daylight, and bringing along with it some of its legendary mist, which the reader, according to his pleasure, may either disregard, or allow it to float almost imperceptibly about the characters and events for the sake of a picturesque effect. +dev-clean/2086/149220/2086_149220_000016_000003|In short, I make pictures out of sunshine; and, not to be too much dazzled with my own trade, I have prevailed with Miss Hepzibah to let me lodge in one of these dusky gables. +dev-clean/2086/149220/2086_149220_000028_000000|Phoebe was on the point of retreating, but turned back, with some hesitation; for she did not exactly comprehend his manner, although, on better observation, its feature seemed rather to be lack of ceremony than any approach to offensive rudeness. +dev-clean/2277/149874/2277_149874_000007_000001|Her husband asked a few questions and sat down to read the evening paper. +dev-clean/2277/149896/2277_149896_000007_000006|He saw only her pretty face and neat figure and wondered why life was not arranged so that such joy as he found with her could be steadily maintained. +dev-clean/2277/149896/2277_149896_000025_000008|He jangled it fiercely several times in succession, but without avail. +dev-clean/2277/149897/2277_149897_000023_000000|"Well?" said Hurstwood. +dev-clean/2277/149897/2277_149897_000046_000002|He troubled over many little details and talked perfunctorily to everybody. +dev-clean/2412/153954/2412_153954_000004_000005|Even in middle age they were still comely, and the old grey haired women at their cottage doors had a dignity, not to say majesty, of their own. +dev-clean/2428/83699/2428_83699_000009_000000|Now it is a remarkable thing that I have always had an extraordinary predilection for the name Madge. +dev-clean/2428/83699/2428_83699_000024_000004|I had long been wishing that an old-fashioned Christmas had been completely extinct before I had thought of adventuring in quest of one. +dev-clean/2428/83699/2428_83699_000047_000000|"Perhaps you had better come inside." +dev-clean/2428/83705/2428_83705_000015_000004|I did not want any unpleasantness; and I am quite sure there would have been unpleasantness had I demurred. +dev-clean/2428/83705/2428_83705_000034_000002|"And what," inquired mrs Macpherson, "has Mary Ann given you?" +dev-clean/251/118436/251_118436_000017_000001|This man was clad in a brown camel hair robe and sandals, and a green turban was on his head. His expression was tranquil, his gaze impersonal. +dev-clean/251/136532/251_136532_000000_000003|Fitzgerald was still trying to find out how the germ had been transmitted. +dev-clean/251/136532/251_136532_000020_000004|Without question, he had become, overnight, the most widely known archaeologist in history. +dev-clean/251/137823/251_137823_000025_000001|Or grazed, at least," Tom added thankfully. +dev-clean/251/137823/251_137823_000054_000002|The two girls were as much upset as Tom's mother. +dev-clean/2803/154320/2803_154320_000017_000004|Think of Lady Glenarvan; think of Mary Grant!" +dev-clean/2803/154328/2803_154328_000028_000000|Wilson and Olbinett joined their companions, and all united to dig through the wall-john with his dagger, the others with stones taken from the ground, or with their nails, while Mulrady, stretched along the ground, watched the native guard through a crevice of the matting. +dev-clean/2803/154328/2803_154328_000080_000003|Where chance led them, but at any rate they were free. +dev-clean/2803/161169/2803_161169_000011_000019|What do you think of that from the coal tar. +dev-clean/2902/9008/2902_9008_000009_000001|He was a Greek, also, but of a more common, and, perhaps, lower type; dark and fiery, thin and graceful; his delicate figure and cheeks, wasted by meditation, harmonised well with the staid and simple philosophic cloak which he wore as a sign of his profession. +dev-clean/2902/9008/2902_9008_000048_000003|For aught I know or care, the plot may be an exactly opposite one, and the Christians intend to murder all the Jews. +dev-clean/3000/15664/3000_15664_000013_000004|These volcanic caves are not wanting in interest, and it is well to light a pitch pine torch and take a walk in these dark ways of the underworld whenever opportunity offers, if for no other reason to see with new appreciation on returning to the sunshine the beauties that lie so thick about us. +dev-clean/3000/15664/3000_15664_000029_000002|Thus the Shasta River issues from a large lake like spring in Shasta Valley, and about two thirds of the volume of the McCloud gushes forth in a grand spring on the east side of the mountain, a few miles back from its immediate base. +dev-clean/3170/137482/3170_137482_000010_000004|The nobility, the merchants, even workmen in good circumstances, are never seen in the 'magazzino', for cleanliness is not exactly worshipped in such places. +dev-clean/3170/137482/3170_137482_000037_000001|He was celebrated in Venice not only for his eloquence and his great talents as a statesman, but also for the gallantries of his youth. +dev-clean/3536/23268/3536_23268_000028_000000|"It is not the first time, I believe, you have acted contrary to that, Miss Milner," replied mrs Horton, and affected a tenderness of voice, to soften the harshness of her words. +dev-clean/3576/138058/3576_138058_000019_000003|He wondered to see the lance leaning against the tree, the shield on the ground, and Don Quixote in armour and dejected, with the saddest and most melancholy face that sadness itself could produce; and going up to him he said, "Be not so cast down, good man, for you have not fallen into the hands of any inhuman Busiris, but into Roque Guinart's, which are more merciful than cruel." +dev-clean/3752/4943/3752_4943_000026_000002|Lie quiet!" +dev-clean/3752/4943/3752_4943_000056_000002|His flogging wouldn't have killed a flea." +dev-clean/3752/4944/3752_4944_000031_000000|"Well now!" said Meekin, with asperity, "I don't agree with you. Everybody seems to be against that poor fellow-Captain Frere tried to make me think that his letters contained a hidden meaning, but I don't believe they did. +dev-clean/3752/4944/3752_4944_000063_000003|He'd rather kill himself." +dev-clean/3752/4944/3752_4944_000094_000000|"The Government may go to----, and you, too!" roared Burgess. +dev-clean/3853/163249/3853_163249_000058_000000|"I've done it, mother: tell me you're not sorry." +dev-clean/3853/163249/3853_163249_000125_000004|Help me to be brave and strong, David: don't let me complain or regret, but show me what lies beyond, and teach me to believe that simply doing the right is reward and happiness enough." +dev-clean/5338/24615/5338_24615_000004_000003|It had been built at a period when castles were no longer necessary, and when the Scottish architects had not yet acquired the art of designing a domestic residence. +dev-clean/5338/284437/5338_284437_000031_000001|A powerful ruler ought to be rich and to live in a splendid palace. +dev-clean/5536/43358/5536_43358_000012_000001|Being a natural man, the Indian was intensely poetical. +dev-clean/5536/43359/5536_43359_000015_000000|The family was not only the social unit, but also the unit of government. +dev-clean/5694/64025/5694_64025_000004_000006|Our regiment was the advance guard on Saturday evening, and did a little skirmishing; but General Gladden's brigade passed us and assumed a position in our immediate front. +dev-clean/5694/64029/5694_64029_000006_000005|I read it, and looked up to hand it back to him, when I discovered that he had a pistol cocked and leveled in my face, and says he, "Drop that gun; you are my prisoner." I saw there was no use in fooling about it. +dev-clean/5694/64029/5694_64029_000024_000002|The ground was literally covered with blue coats dead; and, if I remember correctly, there were eighty dead horses. +dev-clean/5694/64038/5694_64038_000015_000002|I could not imagine what had become of him. +dev-clean/5895/34615/5895_34615_000013_000003|Man can do nothing to create beauty, but everything to produce ugliness. +dev-clean/5895/34615/5895_34615_000025_000000|With this exception, Gwynplaine's laugh was everlasting. +dev-clean/5895/34622/5895_34622_000029_000002|In the opposite corner was the kitchen. +dev-clean/5895/34629/5895_34629_000021_000005|The sea is a wall; and if Voltaire-a thing which he very much regretted when it was too late-had not thrown a bridge over to Shakespeare, Shakespeare might still be in England, on the other side of the wall, a captive in insular glory. +dev-clean/6241/61943/6241_61943_000020_000000|My uncle came out of his cabin pale, haggard, thin, but full of enthusiasm, his eyes dilated with pleasure and satisfaction. +dev-clean/6241/61946/6241_61946_000014_000000|The rugged summits of the rocky hills were dimly visible on the edge of the horizon, through the misty fogs; every now and then some heavy flakes of snow showed conspicuous in the morning light, while certain lofty and pointed rocks were first lost in the grey low clouds, their summits clearly visible above, like jagged reefs rising from a troublous sea. +dev-clean/6241/61946/6241_61946_000051_000001|Then my uncle, myself, and guide, two boatmen and the four horses got into a very awkward flat bottom boat. +dev-clean/6295/64301/6295_64301_000010_000002|The music was broken, and Joseph left alone with the dumb instruments. +dev-clean/6313/66125/6313_66125_000020_000002|"Are you hurt?" +dev-clean/6313/66125/6313_66125_000053_000000|"Are you ready?" +dev-clean/6313/66129/6313_66129_000011_000001|"Cold water is the most nourishing thing we've touched since last night." +dev-clean/6313/66129/6313_66129_000045_000004|Of course, dogs can't follow the trail of an animal as well, now, as they could with snow on the ground. +dev-clean/6313/66129/6313_66129_000081_000000|Stacy dismounted and removed the hat carefully to one side. +dev-clean/6313/76958/6313_76958_000029_000000|Instantly there was a chorus of yells and snarls from the disturbed cowpunchers, accompanied by dire threats as to what they would do to the gopher did he ever disturb their rest in that way again. +dev-clean/6313/76958/6313_76958_000073_000001|"Those fellows have to go out. +dev-clean/6319/275224/6319_275224_000014_000001|And what is the matter with the beautiful straggling branches, that they are to be cut off as fast as they appear? +dev-clean/6319/57405/6319_57405_000019_000000|"It is rather a silly thing to do," said Deucalion; "and yet there can be no harm in it, and we shall see what will happen." +dev-clean/6319/64726/6319_64726_000017_000002|Then the prince took the princess by the hand; she was dressed in great splendour, but he did not hint that she looked as he had seen pictures of his great grandmother look; he thought her all the more charming for that. +dev-clean/6345/93302/6345_93302_000000_000001|All LibriVox recordings are in the public domain. +dev-clean/6345/93302/6345_93302_000049_000000|The fine tact of a noble woman seemed to have deserted her. +dev-clean/6345/93302/6345_93302_000073_000000|So she said- +dev-clean/6345/93306/6345_93306_000024_000002|What is it? +dev-clean/652/130737/652_130737_000031_000001|Good aroma. +dev-clean/7850/111771/7850_111771_000009_000001|After various flanking movements and costly assaults, the problem of taking Lee narrowed itself down to a siege of Petersburg. +dev-clean/7850/281318/7850_281318_000012_000000|She began to show them how to weave the bits of things together into nests, as they should be made. +dev-clean/7850/286674/7850_286674_000006_000001|You would think that, with six legs apiece and three joints in each leg, they might walk quite fast, yet they never did. +dev-clean/7850/73752/7850_73752_000006_000003|What a Neapolitan ball was his career then! +dev-clean/7976/105575/7976_105575_000009_000000|The burying party the next morning found nineteen dead Rebels lying together at one place. +dev-clean/7976/105575/7976_105575_000017_000000|Our regiment now pursued the flying Rebels with great vigor. +dev-clean/7976/110124/7976_110124_000021_000001|"We two are older and wiser than you are. It is for us to determine what shall be done. +dev-clean/7976/110124/7976_110124_000053_000002|The doors were strong and held securely. +dev-clean/7976/110523/7976_110523_000027_000000|"We will go in here," said Hansel, "and have a glorious feast. +dev-clean/8297/275154/8297_275154_000008_000000|Was this man-haggard, pallid, shabby, looking at him piteously with bloodshot eyes-the handsome, pleasant, prosperous brother whom he remembered? +dev-clean/8297/275154/8297_275154_000024_000011|Tell me where my wife is living now?" +dev-clean/8297/275155/8297_275155_000013_000006|What a perfect gentleman!" +dev-clean/8297/275155/8297_275155_000037_000000|"Say thoroughly worthy of the course forced upon me and my daughter by your brother's infamous conduct-and you will be nearer the mark!" +dev-clean/8297/275156/8297_275156_000013_000005|No more of it now. +dev-clean/84/121123/84_121123_000009_000000|But in less than five minutes the staircase groaned beneath an extraordinary weight. +dev-clean/84/121123/84_121123_000054_000000|It was something terrible to witness the silent agony, the mute despair of Noirtier, whose tears silently rolled down his cheeks. +dev-clean/84/121550/84_121550_000064_000000|And lo! a sudden lustre ran across On every side athwart the spacious forest, Such that it made me doubt if it were lightning. +dev-clean/84/121550/84_121550_000156_000000|Nor prayer for inspiration me availed, By means of which in dreams and otherwise I called him back, so little did he heed them. +dev-clean/84/121550/84_121550_000247_000000|Thus Beatrice; and I, who at the feet Of her commandments all devoted was, My mind and eyes directed where she willed. +dev-clean/8842/302203/8842_302203_000001_000001|And I remember that on the ninth day, being overcome with intolerable pain, a thought came into my mind concerning my lady: but when it had a little nourished this thought, my mind returned to its brooding over mine enfeebled body. diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/dev-other.txt b/src/third_party/BigVGAN/filelists/LibriTTS/dev-other.txt new file mode 100644 index 0000000000000000000000000000000000000000..af85b4fe40a33f04445abf34576457bf7ad150a0 --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/dev-other.txt @@ -0,0 +1,93 @@ +dev-other/116/288045/116_288045_000003_000000|PART one +dev-other/116/288045/116_288045_000034_000001|He was only an idol. +dev-other/116/288047/116_288047_000002_000002|Observing the sun, the moon, and the stars overhead, the primitive man wished to account for them. +dev-other/116/288048/116_288048_000001_000000|Let me now give an idea of the method I propose to follow in the study of this subject. +dev-other/116/288048/116_288048_000020_000003|Leaving out Judas, and counting Matthias, who was elected in his place, we have thirteen apostles. +dev-other/1255/138279/1255_138279_000012_000000|"One. +dev-other/1255/138279/1255_138279_000049_000001|Will it be by banns or license?" +dev-other/1255/74899/1255_74899_000020_000000|"Pardon me. +dev-other/1255/90407/1255_90407_000006_000001|But, as the rain gave not the least sign of cessation, he observed: 'I think we shall have to go back.' +dev-other/1255/90407/1255_90407_000039_000002|Into it they plodded without pause, crossing the harbour bridge about midnight, wet to the skin. +dev-other/1255/90413/1255_90413_000023_000001|'Now what the devil this means I cannot tell,' he said to himself, reflecting stock still for a moment on the stairs. +dev-other/1585/131718/1585_131718_000025_000009|Edison marginalized documents extensively. +dev-other/1630/141772/1630_141772_000000_000002|Suddenly he again felt that he was alive and suffering from a burning, lacerating pain in his head. +dev-other/1630/141772/1630_141772_000039_000000|The quiet home life and peaceful happiness of Bald Hills presented itself to him. +dev-other/1630/73710/1630_73710_000019_000003|I almost wish papa would return, though I dread to see him. +dev-other/1630/96099/1630_96099_000033_000001|Why did you not follow him? +dev-other/1650/157641/1650_157641_000037_000001|mr w m +dev-other/1650/173551/1650_173551_000025_000000|Pierre went into that gloomy study which he had entered with such trepidation in his benefactor's lifetime. +dev-other/1651/136854/1651_136854_000046_000005|I have, however, this of gratitude, that I think of you with regard, when I do not, perhaps, give the proofs which I ought, of being, Sir, +dev-other/1686/142278/1686_142278_000015_000000|'No! not doubts as to religion; not the slightest injury to that.' He paused. +dev-other/1686/142278/1686_142278_000042_000001|Margaret was nearly upset again into a burst of crying. +dev-other/1701/141759/1701_141759_000001_000001|Not till midwinter was the count at last handed a letter addressed in his son's handwriting. +dev-other/1701/141759/1701_141759_000048_000000|"Why should you be ashamed?" +dev-other/1701/141760/1701_141760_000013_000003|"I only sent you the note yesterday by Bolkonski-an adjutant of Kutuzov's, who's a friend of mine. +dev-other/1701/141760/1701_141760_000056_000000|In spite of Prince Andrew's disagreeable, ironical tone, in spite of the contempt with which Rostov, from his fighting army point of view, regarded all these little adjutants on the staff of whom the newcomer was evidently one, Rostov felt confused, blushed, and became silent. +dev-other/2506/13150/2506_13150_000022_000000|--Nay-if you don't believe me, you may read the chapter for your pains. +dev-other/3660/172182/3660_172182_000012_000007|And a year, and a second, and a third, he proceeded thus, until his fame had flown over the face of the kingdom. +dev-other/3660/172183/3660_172183_000011_000000|So the maiden went forward, keeping in advance of Geraint, as he had desired her; and it grieved him as much as his wrath would permit, to see a maiden so illustrious as she having so much trouble with the care of the horses. +dev-other/3660/172183/3660_172183_000019_000040|Come with me to the court of a son in law of my sister, which is near here, and thou shalt have the best medical assistance in the kingdom." +dev-other/3660/6517/3660_6517_000036_000002|Bright sunshine. +dev-other/3660/6517/3660_6517_000059_000005|Not a single one has lost his good spirits. +dev-other/3663/172005/3663_172005_000022_000000|She must cross the Slide Brook valley, if possible, and gain the mountain opposite. +dev-other/3663/172528/3663_172528_000016_000008|He had been brought by my very dear friend Luca Martini, who passed the larger portion of the day with me. +dev-other/3915/57461/3915_57461_000018_000001|In a fit of madness I was tempted to kill and rob you. +dev-other/3915/98647/3915_98647_000018_000006|Thus the old custom is passing away. +dev-other/4323/13259/4323_13259_000009_000011|What would Jesus do? +dev-other/4323/13259/4323_13259_000020_000003|It seems she had been recently converted during the evangelist's meetings, and was killed while returning from one of the meetings in company with other converts and some of her friends. +dev-other/4323/18416/4323_18416_000019_000001|So she was asked to sing at musicales and receptions without end, until Alexia exclaimed at last, "They are all raving, stark mad over her, and it's all Polly's own fault, the whole of it." +dev-other/4323/18416/4323_18416_000050_000000|"I know, child; you think your old Grandpapa does just about right," said mr King soothingly, and highly gratified. +dev-other/4323/18416/4323_18416_000079_000002|"And I can't tolerate any thoughts I cannot speak." +dev-other/4323/55228/4323_55228_000028_000000|"Pete told you that I didn't care for any girl, only to paint?" demanded Bertram, angry and mystified. +dev-other/4323/55228/4323_55228_000071_000000|There was another silence. +dev-other/4570/102353/4570_102353_000001_000000|CHAPTER four. +dev-other/4570/14911/4570_14911_000009_000002|EYES-Brown, dark hazel or hazel, not deep set nor bulgy, and with a mild expression. +dev-other/4570/56594/4570_56594_000012_000000|"'No,' says the gentleman. +dev-other/4831/18525/4831_18525_000028_000000|"Oh! isn't it 'Oats, Peas, Beans, and Barley grow'?" cried Polly, as they watched them intently. +dev-other/4831/18525/4831_18525_000078_000001|"I want to write, too, I do," she cried, very much excited. +dev-other/4831/18525/4831_18525_000122_000000|"O dear me!" exclaimed Polly, softly, for she couldn't even yet get over that dreadful beginning. +dev-other/4831/25894/4831_25894_000022_000003|The other days were very much like this; sometimes they made more, sometimes less, but Tommo always 'went halves;' and Tessa kept on, in spite of cold and weariness, for her plans grew as her earnings increased, and now she hoped to get useful things, instead of candy and toys alone. +dev-other/4831/29134/4831_29134_000001_000000|The session was drawing toward its close. +dev-other/4831/29134/4831_29134_000018_000000|"So this poor little boy grew up to be a man, and had to go out in the world, far from home and friends to earn his living. +dev-other/5543/27761/5543_27761_000019_000000|Her mother went to hide. +dev-other/5543/27761/5543_27761_000065_000000|"Agathya says so, madam," answered Fedosya; "it's she that knows." +dev-other/5543/27761/5543_27761_000107_000000|"Sima, my dear, don't agitate yourself," said Sergey Modestovich in a whisper. +dev-other/5849/50873/5849_50873_000026_000000|"He has promised to do so." +dev-other/5849/50873/5849_50873_000074_000000|"The boy did it! +dev-other/5849/50962/5849_50962_000010_000000|"It's a schooner," said mr Bingham to mr Minturn, "and she has a very heavy cargo." +dev-other/5849/50963/5849_50963_000009_000003|Well, it was a long, slow job to drag those heavy logs around that point, and just when we were making headway, along comes a storm that drove the schooner and canoes out of business." +dev-other/5849/50964/5849_50964_000018_000001|There were the shells to be looked after, the fish nets, besides Downy, the duck, and Snoop, the cat. +dev-other/6123/59150/6123_59150_000016_000001|He kicked him two or three times with his heel in the face. +dev-other/6123/59186/6123_59186_000008_000000|"Catering care" is an appalling phrase. +dev-other/6267/53049/6267_53049_000007_000001|"I'd better be putting my grey matter into that algebra instead of wasting it plotting for a party dress that I certainly can't get. +dev-other/6267/53049/6267_53049_000045_000001|I am named after her." +dev-other/6267/65525/6267_65525_000018_000000|Dear mr Lincoln: +dev-other/6267/65525/6267_65525_000045_000006|You can't mistake it." +dev-other/6455/66379/6455_66379_000020_000002|(Deal, sir, if you please; better luck next time.)" +dev-other/6455/67803/6455_67803_000038_000000|"Yes," he answered. +dev-other/6467/56885/6467_56885_000012_000001|As you are so generously taking her on trust, may she never cause you a moment's regret. +dev-other/6467/97061/6467_97061_000010_000000|A terrible battle ensued, in which both kings performed prodigies of valour. +dev-other/6841/88291/6841_88291_000006_000006|One stood waiting for them to finish, a sheaf of long j h stamping irons in his hand. +dev-other/6841/88291/6841_88291_000019_000006|Cries arose in a confusion: "Marker" "Hot iron!" "Tally one!" Dust eddied and dissipated. +dev-other/6841/88294/6841_88294_000010_000003|Usually I didn't bother with his talk, for it didn't mean anything, but something in his voice made me turn. +dev-other/6841/88294/6841_88294_000048_000000|He stood there looking straight at me without winking or offering to move. +dev-other/700/122866/700_122866_000006_000003|You've been thirteen for a month, so I suppose it doesn't seem such a novelty to you as it does to me. +dev-other/700/122866/700_122866_000023_000006|Ruby Gillis is rather sentimental. +dev-other/700/122867/700_122867_000012_000004|My career is closed. +dev-other/700/122867/700_122867_000033_000003|At the end of the week Marilla said decidedly: +dev-other/700/122868/700_122868_000015_000003|mrs Lynde says that all play acting is abominably wicked." +dev-other/700/122868/700_122868_000038_000001|And Ruby is in hysterics-oh, Anne, how did you escape?" +dev-other/7601/101622/7601_101622_000018_000002|The very girls themselves set them on: +dev-other/7601/175351/7601_175351_000031_000008|Still, during the nights which followed the fifteenth of August, darkness was never profound; although the sun set, he still gave sufficient light by refraction. +dev-other/7641/96252/7641_96252_000003_000006|For these are careful only for themselves, for their own egoism, just like the bandit, from whom they are only distinguished by the absurdity of their means. +dev-other/7641/96670/7641_96670_000013_000001|The mist lifted suddenly and she saw three strangers in the palace courtyard. +dev-other/7641/96684/7641_96684_000009_000000|"What years of happiness have been mine, O Apollo, through your friendship for me," said Admetus. +dev-other/7641/96684/7641_96684_000031_000002|How noble it was of Admetus to bring him into his house and give entertainment to him while such sorrow was upon him. +dev-other/7697/105815/7697_105815_000048_000002|And they brought out the jaw bone of an ass with which Samson did such great feats, and the sling and stone with which David slew Goliath of Gath. +dev-other/8173/294714/8173_294714_000006_000001|"Don't spoil my pleasure in seeing you again by speaking of what can never be! Have you still to be told how it is that you find me here alone with my child?" +dev-other/8173/294714/8173_294714_000027_000001|What was there to prevent her from insuring her life, if she pleased, and from so disposing of the insurance as to give Van Brandt a direct interest in her death? +dev-other/8254/115543/8254_115543_000034_000000|"Yes, and how he orders every one about him. +dev-other/8254/84205/8254_84205_000029_000000|"I'm not afraid of them hitting me, my lad," said Griggs confidently. "Being shot at by fellows with bows and arrows sounds bad enough, but there's not much risk here." +dev-other/8254/84205/8254_84205_000073_000000|"Right; I do, neighbour, and it's very handsome of you to offer me the chance to back out. +dev-other/8288/274162/8288_274162_000023_000000|"Exactly. +dev-other/8288/274162/8288_274162_000078_000000|"So much the worse. diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/parse_libritts.py b/src/third_party/BigVGAN/filelists/LibriTTS/parse_libritts.py new file mode 100644 index 0000000000000000000000000000000000000000..ec5f64d298ab59b594b0ac70e53f26df74e9b4ad --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/parse_libritts.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import os, glob + + +def get_wav_and_text_filelist(data_root, data_type, subsample=1): + wav_list = sorted( + [ + path.replace(data_root, "")[1:] + for path in glob.glob(os.path.join(data_root, data_type, "**/**/*.wav")) + ] + ) + wav_list = wav_list[::subsample] + txt_filelist = [path.replace(".wav", ".normalized.txt") for path in wav_list] + + txt_list = [] + for txt_file in txt_filelist: + with open(os.path.join(data_root, txt_file), "r") as f_txt: + text = f_txt.readline().strip("\n") + txt_list.append(text) + wav_list = [path.replace(".wav", "") for path in wav_list] + + return wav_list, txt_list + + +def write_filelist(output_path, wav_list, txt_list): + with open(output_path, "w") as f: + for i in range(len(wav_list)): + filename = wav_list[i] + "|" + txt_list[i] + f.write(filename + "\n") + + +if __name__ == "__main__": + + data_root = "filelists/LibriTTS" + + # Dev and test sets. subsample each sets to get ~100 utterances + data_type_list = ["dev-clean", "dev-other", "test-clean", "test-other"] + subsample_list = [50, 50, 50, 50] + for data_type, subsample in zip(data_type_list, subsample_list): + print(f"processing {data_type}") + data_path = os.path.join(data_root, data_type) + assert os.path.exists(data_path), ( + f"path {data_path} not found. make sure the path is accessible by creating the symbolic link using the following command: " + f"ln -s /path/to/your/{data_path} {data_path}" + ) + wav_list, txt_list = get_wav_and_text_filelist(data_root, data_type, subsample) + write_filelist(os.path.join(data_root, data_type + ".txt"), wav_list, txt_list) + + # Training and seen speaker validation datasets (libritts-full): train-clean-100 + train-clean-360 + train-other-500 + wav_list_train, txt_list_train = [], [] + for data_type in ["train-clean-100", "train-clean-360", "train-other-500"]: + print(f"processing {data_type}") + data_path = os.path.join(data_root, data_type) + assert os.path.exists(data_path), ( + f"path {data_path} not found. make sure the path is accessible by creating the symbolic link using the following command: " + f"ln -s /path/to/your/{data_path} {data_path}" + ) + wav_list, txt_list = get_wav_and_text_filelist(data_root, data_type) + wav_list_train.extend(wav_list) + txt_list_train.extend(txt_list) + + # Split the training set so that the seen speaker validation set contains ~100 utterances + subsample_val = 3000 + wav_list_val, txt_list_val = ( + wav_list_train[::subsample_val], + txt_list_train[::subsample_val], + ) + del wav_list_train[::subsample_val] + del txt_list_train[::subsample_val] + write_filelist( + os.path.join(data_root, "train-full.txt"), wav_list_train, txt_list_train + ) + write_filelist(os.path.join(data_root, "val-full.txt"), wav_list_val, txt_list_val) + + print("done") diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/test-clean.txt b/src/third_party/BigVGAN/filelists/LibriTTS/test-clean.txt new file mode 100644 index 0000000000000000000000000000000000000000..13effbf405736ce8b43e07f11b87445d1b5ffaa3 --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/test-clean.txt @@ -0,0 +1,97 @@ +test-clean/1089/134686/1089_134686_000001_000001|He hoped there would be stew for dinner, turnips and carrots and bruised potatoes and fat mutton pieces to be ladled out in thick peppered flour fattened sauce. Stuff it into you, his belly counselled him. +test-clean/1089/134686/1089_134686_000020_000001|We can scut the whole hour. +test-clean/1089/134691/1089_134691_000004_000001|Yet her mistrust pricked him more keenly than his father's pride and he thought coldly how he had watched the faith which was fading down in his soul ageing and strengthening in her eyes. +test-clean/1089/134691/1089_134691_000027_000004|Now, at the name of the fabulous artificer, he seemed to hear the noise of dim waves and to see a winged form flying above the waves and slowly climbing the air. +test-clean/1188/133604/1188_133604_000018_000002|There are just four touches-fine as the finest penmanship-to do that beak; and yet you will find that in the peculiar paroquettish mumbling and nibbling action of it, and all the character in which this nibbling beak differs from the tearing beak of the eagle, it is impossible to go farther or be more precise. +test-clean/121/121726/121_121726_000046_000003|Tied to a woman. +test-clean/121/127105/121_127105_000024_000000|He laughed for the first time. +test-clean/1284/1180/1284_1180_000001_000000|The Crooked Magician +test-clean/1284/1181/1284_1181_000005_000000|The head of the Patchwork Girl was the most curious part of her. +test-clean/1320/122612/1320_122612_000019_000005|It is true that the horses are here, but the Hurons are gone; let us, then, hunt for the path by which they parted." +test-clean/1320/122612/1320_122612_000056_000002|Then he reappeared, creeping along the earth, from which his dress was hardly distinguishable, directly in the rear of his intended captive. +test-clean/1580/141083/1580_141083_000012_000000|"The first page on the floor, the second in the window, the third where you left it," said he. +test-clean/1580/141083/1580_141083_000041_000003|Above were three students, one on each story. +test-clean/1580/141083/1580_141083_000063_000001|Holmes held it out on his open palm in the glare of the electric light. +test-clean/1580/141083/1580_141083_000110_000001|Where were you when you began to feel bad?" +test-clean/1580/141084/1580_141084_000024_000002|Pencils, too, and knives-all was satisfactory. +test-clean/1580/141084/1580_141084_000060_000001|"I frankly admit that I am unable to prove it. +test-clean/1580/141084/1580_141084_000085_000000|"Good heavens! have you nothing to add?" cried Soames. +test-clean/1995/1826/1995_1826_000022_000001|Miss Taylor did not know much about cotton, but at least one more remark seemed called for. +test-clean/1995/1836/1995_1836_000016_000001|No, of course there was no immediate danger; but when people were suddenly thrust beyond their natural station, filled with wild ideas and impossible ambitions, it meant terrible danger to Southern white women. +test-clean/1995/1837/1995_1837_000024_000000|He heard that she was down stairs and ran to meet her with beating heart. +test-clean/2300/131720/2300_131720_000016_000005|Having travelled around the world, I had cultivated an indifference to any special difficulties of that kind. +test-clean/2300/131720/2300_131720_000030_000005|I telephoned again, and felt something would happen, but fortunately it did not. +test-clean/237/126133/237_126133_000002_000004|It got to be noticed finally; and one and all redoubled their exertions to make everything twice as pleasant as ever! +test-clean/237/126133/237_126133_000049_000000|But the chubby face didn't look up brightly, as usual: and the next moment, without a bit of warning, Phronsie sprang past them all, even Polly, and flung herself into mr King's arms, in a perfect torrent of sobs. +test-clean/237/134493/237_134493_000008_000003|Alexandra lets you sleep late. +test-clean/237/134500/237_134500_000001_000001|Frank sat up until a late hour reading the Sunday newspapers. +test-clean/237/134500/237_134500_000014_000000|"I don't know all of them, but I know lindens are. +test-clean/237/134500/237_134500_000034_000000|She sighed despondently. +test-clean/260/123286/260_123286_000019_000002|Therefore don't talk to me about views and prospects." +test-clean/260/123286/260_123286_000049_000005|He shakes his head negatively. +test-clean/260/123288/260_123288_000016_000002|It rushes on from the farthest recesses of the vast cavern. +test-clean/260/123288/260_123288_000043_000001|I could just see my uncle at full length on the raft, and Hans still at his helm and spitting fire under the action of the electricity which has saturated him. +test-clean/2830/3979/2830_3979_000007_000000|PREFACE +test-clean/2830/3980/2830_3980_000018_000001|Humble man that he was, he will not now take a back seat. +test-clean/2961/961/2961_961_000004_000037|Then your city did bravely, and won renown over the whole earth. +test-clean/2961/961/2961_961_000023_000003|But violent as were the internal and alimentary fluids, the tide became still more violent when the body came into contact with flaming fire, or the solid earth, or gliding waters, or the stormy wind; the motions produced by these impulses pass through the body to the soul and have the name of sensations. +test-clean/3570/5694/3570_5694_000009_000003|The canon of reputability is at hand and seizes upon such innovations as are, according to its standard, fit to survive. +test-clean/3570/5695/3570_5695_000001_000003|But the middle class wife still carries on the business of vicarious leisure, for the good name of the household and its master. +test-clean/3570/5695/3570_5695_000009_000005|Considered by itself simply-taken in the first degree-this added provocation to which the artisan and the urban laboring classes are exposed may not very seriously decrease the amount of savings; but in its cumulative action, through raising the standard of decent expenditure, its deterrent effect on the tendency to save cannot but be very great. +test-clean/3570/5696/3570_5696_000011_000006|For this is the basis of award of the instinct of workmanship, and that instinct is the court of final appeal in any question of economic truth or adequacy. +test-clean/3729/6852/3729_6852_000004_000003|In order to please her, I spoke to her of the Abbe Conti, and I had occasion to quote two lines of that profound writer. +test-clean/4077/13754/4077_13754_000002_000000|The troops, once in Utah, had to be provisioned; and everything the settlers could spare was eagerly bought at an unusual price. The gold changed hands. +test-clean/4446/2271/4446_2271_000003_000004|There's everything in seeing Hilda while she's fresh in a part. +test-clean/4446/2271/4446_2271_000020_000001|Lady Westmere is very fond of Hilda." +test-clean/4446/2273/4446_2273_000008_000002|I've no need for fine clothes in Mac's play this time, so I can afford a few duddies for myself. +test-clean/4446/2273/4446_2273_000027_000004|She did my blouses beautifully the last time I was there, and was so delighted to see me again. +test-clean/4446/2273/4446_2273_000046_000001|"Aren't you afraid to let the wind low like that on your neck? +test-clean/4446/2275/4446_2275_000013_000000|Hilda was pale by this time, and her eyes were wide with fright. +test-clean/4446/2275/4446_2275_000038_000006|"You want to tell me that you can only see me like this, as old friends do, or out in the world among people? +test-clean/4507/16021/4507_16021_000011_000000|It engenders a whole world, la pegre, for which read theft, and a hell, la pegrenne, for which read hunger. +test-clean/4507/16021/4507_16021_000030_000001|Facts form one of these, and ideas the other. +test-clean/4970/29093/4970_29093_000010_000000|Delightful illusion of paint and tinsel and silk attire, of cheap sentiment and high and mighty dialogue! +test-clean/4970/29093/4970_29093_000047_000000|"Never mind the map. +test-clean/4970/29095/4970_29095_000021_000000|"I will practice it." +test-clean/4970/29095/4970_29095_000055_000002|He took it with him from the Southern Hotel, when he went to walk, and read it over and again in an unfrequented street as he stumbled along. +test-clean/4992/41797/4992_41797_000014_000002|He keeps the thou shalt not commandments first rate, Hen Lord does! +test-clean/4992/41806/4992_41806_000020_000001|Thou who settest the solitary in families, bless the life that is sheltered here. +test-clean/5105/28241/5105_28241_000004_000004|The late astounding events, however, had rendered Procope manifestly uneasy, and not the less so from his consciousness that the count secretly partook of his own anxiety. +test-clean/5142/33396/5142_33396_000004_000004|At the prow I carved the head with open mouth and forked tongue thrust out. +test-clean/5142/33396/5142_33396_000039_000000|"The thralls were bringing in a great pot of meat. +test-clean/5142/36377/5142_36377_000013_000003|I liked Naomi Colebrook at first sight; liked her pleasant smile; liked her hearty shake of the hand when we were presented to each other. +test-clean/5639/40744/5639_40744_000003_000006|Mother! dear father! do you hear me? +test-clean/5639/40744/5639_40744_000022_000000|Just then Leocadia came to herself, and embracing the cross seemed changed into a sea of tears, and the gentleman remained in utter bewilderment, until his wife had repeated to him, from beginning to end, Leocadia's whole story; and he believed it, through the blessed dispensation of Heaven, which had confirmed it by so many convincing testimonies. +test-clean/5683/32865/5683_32865_000018_000000|Well, it was pretty-French, I dare say-a little set of tablets-a toy-the cover of enamel, studded in small jewels, with a slender border of symbolic flowers, and with a heart in the centre, a mosaic of little carbuncles, rubies, and other red and crimson stones, placed with a view to light and shade. +test-clean/5683/32866/5683_32866_000005_000000|'Did you see that?' said Wylder in my ear, with a chuckle; and, wagging his head, he added, rather loftily for him, 'Miss Brandon, I reckon, has taken your measure, Master Stanley, as well as i I wonder what the deuce the old dowager sees in him. +test-clean/5683/32866/5683_32866_000047_000002|I was not a bit afraid of being found out. +test-clean/5683/32879/5683_32879_000036_000002|Be he near, or be he far, I regard his very name with horror.' +test-clean/6829/68769/6829_68769_000011_000000|So as soon as breakfast was over the next morning Beth and Kenneth took one of the automobiles, the boy consenting unwillingly to this sort of locomotion because it would save much time. +test-clean/6829/68769/6829_68769_000051_000001|One morning she tried to light the fire with kerosene, and lost her sight. +test-clean/6829/68769/6829_68769_000089_000001|Why should you do all this?" +test-clean/6829/68771/6829_68771_000018_000003|A speakers' stand, profusely decorated, had been erected on the lawn, and hundreds of folding chairs provided for seats. +test-clean/6930/75918/6930_75918_000000_000001|Night. +test-clean/6930/81414/6930_81414_000041_000001|Here is his scarf, which has evidently been strained, and on it are spots of blood, while all around are marks indicating a struggle. +test-clean/7021/79740/7021_79740_000010_000006|I observe that, when you both wish for the same thing, you don't quarrel for it and try to pull it away from one another; but one waits like a lady until the other has done with it. +test-clean/7021/85628/7021_85628_000017_000000|"I am going to the court ball," answered Anders. +test-clean/7127/75946/7127_75946_000022_000002|It is necessary, therefore, that he should comply." +test-clean/7127/75946/7127_75946_000061_000001|Disdainful of a success of which Madame showed no acknowledgement, he thought of nothing but boldly regaining the marked preference of the princess. +test-clean/7127/75947/7127_75947_000035_000000|"Quite true, and I believe you are right. +test-clean/7176/88083/7176_88083_000002_000003|He was too imposing in appearance, too gorgeous in apparel, too bold and vigilant in demeanor to be so misunderstood. +test-clean/7176/88083/7176_88083_000017_000000|Immediately over his outstretched gleaming head flew the hawk. +test-clean/7176/92135/7176_92135_000011_000000|And, so on in the same vein for some thirty lines. +test-clean/7176/92135/7176_92135_000074_000001|Tea, please, Matthews. +test-clean/7729/102255/7729_102255_000011_000003|The Free State Hotel served as barracks. +test-clean/7729/102255/7729_102255_000028_000009|They were squads of Kansas militia, companies of "peaceful emigrants," or gangs of irresponsible outlaws, to suit the chance, the whim, or the need of the moment. +test-clean/8230/279154/8230_279154_000003_000002|In the present lecture I shall attempt the analysis of memory knowledge, both as an introduction to the problem of knowledge in general, and because memory, in some form, is presupposed in almost all other knowledge. +test-clean/8230/279154/8230_279154_000013_000003|One of these is context. +test-clean/8230/279154/8230_279154_000027_000000|A further stage is RECOGNITION. +test-clean/8455/210777/8455_210777_000022_000003|And immediately on his sitting down, there got up a gentleman to whom I had not been introduced before this day, and gave the health of Mrs Neverbend and the ladies of Britannula. +test-clean/8455/210777/8455_210777_000064_000001|Government that he shall be treated with all respect, and that those honours shall be paid to him which are due to the President of a friendly republic. +test-clean/8463/287645/8463_287645_000023_000001|For instance, Jacob Taylor was noticed on the record book as being twenty three years of age, and the name of his master was entered as "William Pollit;" but as Jacob had never been allowed to learn to read, he might have failed in giving a correct pronunciation of the name. +test-clean/8463/294825/8463_294825_000048_000000|- CENTIMETER Roughly two fifths of an inch +test-clean/8463/294828/8463_294828_000046_000001|Conseil did them in a flash, and I was sure the lad hadn't missed a thing, because he classified shirts and suits as expertly as birds and mammals. +test-clean/8555/284447/8555_284447_000018_000002|The poor Queen, by the way, was seldom seen, as she passed all her time playing solitaire with a deck that was one card short, hoping that before she had lived her entire six hundred years she would win the game. +test-clean/8555/284447/8555_284447_000049_000000|Now, indeed, the Boolooroo was as angry as he was amazed. +test-clean/8555/284449/8555_284449_000039_000000|When the courtiers and the people assembled saw the goat they gave a great cheer, for the beast had helped to dethrone their wicked Ruler. +test-clean/8555/292519/8555_292519_000041_000000|She was alone that night. He had broken into her courtyard. Above the gurgling gutters he heard- surely- a door unchained? diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/test-other.txt b/src/third_party/BigVGAN/filelists/LibriTTS/test-other.txt new file mode 100644 index 0000000000000000000000000000000000000000..8e083a4fa6951c9c83b8c708b242592b9a8fa151 --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/test-other.txt @@ -0,0 +1,103 @@ +test-other/1688/142285/1688_142285_000000_000000|'Margaret!' said mr Hale, as he returned from showing his guest downstairs; 'I could not help watching your face with some anxiety, when mr Thornton made his confession of having been a shop boy. +test-other/1688/142285/1688_142285_000046_000000|'No, mamma; that Anne Buckley would never have done.' +test-other/1998/15444/1998_15444_000012_000000|Simple filtration will sometimes suffice to separate the required substance; in other cases dialysis will be necessary, in order that crystalloid substances may be separated from colloid bodies. +test-other/1998/29454/1998_29454_000021_000001|Fried eggs and bacon-he had one egg and the man had three-bread and butter-and if the bread was thick, so was the butter-and as many cups of tea as you liked to say thank you for. +test-other/1998/29454/1998_29454_000053_000001|It almost looked, Dickie thought, as though he had brought them out for some special purpose. +test-other/1998/29455/1998_29455_000022_000000|It was a wonderful day. +test-other/1998/29455/1998_29455_000082_000003|But 'e's never let it out." +test-other/2414/128292/2414_128292_000003_000000|"What!" said he, "have not the most ludicrous things always happened to us old anchorites and saints? +test-other/2609/156975/2609_156975_000036_000004|The cruel fate of his people and the painful experience in Egypt that had driven him into the wilderness prepared his mind to receive this training. +test-other/3005/163389/3005_163389_000017_000001|And they laughed all the time, and that made the duke mad; and everybody left, anyway, before the show was over, but one boy which was asleep. +test-other/3005/163390/3005_163390_000023_000021|S'pose people left money laying around where he was what did he do? +test-other/3005/163391/3005_163391_000021_000000|"It's a pretty long journey. +test-other/3005/163399/3005_163399_000013_000002|When we got there she set me down in a split bottomed chair, and set herself down on a little low stool in front of me, holding both of my hands, and says: +test-other/3005/163399/3005_163399_000045_000000|He sprung to the window at the head of the bed, and that give mrs Phelps the chance she wanted. +test-other/3080/5040/3080_5040_000000_000010|You have no such ladies in Ireland? +test-other/3331/159605/3331_159605_000006_000002|I could do so much for all at home how I should enjoy that!" And Polly let her thoughts revel in the luxurious future her fancy painted. +test-other/3331/159605/3331_159605_000082_000000|"Who got up that nice idea, I should like to know?" demanded Polly, as Fanny stopped for breath. +test-other/3528/168656/3528_168656_000003_000003|She told wonders of the Abbey of Fontevrault,--that it was like a city, and that there were streets in the monastery. +test-other/3528/168669/3528_168669_000030_000000|A silence ensued. +test-other/3528/168669/3528_168669_000075_000000|"Like yourself, reverend Mother." +test-other/3528/168669/3528_168669_000123_000000|"But the commissary of police-" +test-other/3528/168669/3528_168669_000137_000000|"That is well." +test-other/3528/168669/3528_168669_000164_000008|I shall have my lever. +test-other/3538/142836/3538_142836_000021_000003|However, as late as the reigns of our two last Georges, fabulous sums were often expended upon fanciful desserts. +test-other/3538/163619/3538_163619_000054_000000|'Now he says that you are to make haste and throw yourself overboard,' answered the step mother. +test-other/3538/163622/3538_163622_000069_000000|So they travelled onwards again, for many and many a mile, over hill and dale. +test-other/3538/163624/3538_163624_000038_000000|Then Sigurd went down into that deep place, and dug many pits in it, and in one of the pits he lay hidden with his sword drawn. +test-other/367/130732/367_130732_000002_000001|Probably nowhere in San Francisco could one get lobster better served than in the Old Delmonico restaurant of the days before the fire. +test-other/3764/168670/3764_168670_000003_000000|"But you, Father Madeleine?" +test-other/3764/168670/3764_168670_000043_000000|"Yes." +test-other/3764/168670/3764_168670_000083_000005|He grumbled:-- +test-other/3764/168671/3764_168671_000012_000003|He did what he liked with him. +test-other/3764/168671/3764_168671_000046_000000|"Comrade!" cried Fauchelevent. +test-other/3997/180294/3997_180294_000023_000000|Then, when God allows love to a courtesan, that love, which at first seems like a pardon, becomes for her almost without penitence. +test-other/3997/180294/3997_180294_000065_000001|The count will be coming back, and there is nothing to be gained by his finding you here." +test-other/3997/180297/3997_180297_000034_000004|For these people we have to be merry when they are merry, well when they want to sup, sceptics like themselves. +test-other/3997/182399/3997_182399_000014_000003|Oh, my, no! +test-other/4198/61336/4198_61336_000000_000003|It is significant to note in this connection that the new king was an unswerving adherent of the cult of Ashur, by the adherents of which he was probably strongly supported. +test-other/4198/61336/4198_61336_000033_000001|Nabonassar had died and was succeeded by his son Nabu nadin zeri, who, after reigning for two years, was slain in a rebellion. +test-other/4294/14317/4294_14317_000022_000011|I do not condescend to smite you. He looked at me submissively and said nothing. +test-other/4294/35475/4294_35475_000018_000001|At last they reached a wide chasm that bounded the Ogre's domain. +test-other/4294/35475/4294_35475_000050_000002|They said, "We are only waiting to lay some wily plan to capture the Ogre." +test-other/4294/9934/4294_9934_000025_000000|"Gold; here it is." +test-other/4350/10919/4350_10919_000006_000000|"Immediately, princess. +test-other/4350/9170/4350_9170_000005_000001|Authority, in the sense in which the word is ordinarily understood, is a means of forcing a man to act in opposition to his desires. +test-other/4350/9170/4350_9170_000056_000000|But the fatal significance of universal military service, as the manifestation of the contradiction inherent in the social conception of life, is not only apparent in that. +test-other/4852/28311/4852_28311_000031_000001|After a step or two, not finding his friend beside him, he turned. +test-other/4852/28319/4852_28319_000013_000002|mr Wicker waited patiently beside him for a few moments for Chris to get up his courage. +test-other/533/1066/533_1066_000008_000000|"I mean," he persisted, "do you feel as though you could go through with something rather unusual?" +test-other/533/131562/533_131562_000018_000000|mr Huntingdon then went up stairs. +test-other/5442/41168/5442_41168_000002_000001|Sergey Ivanovitch, waiting till the malignant gentleman had finished speaking, said that he thought the best solution would be to refer to the act itself, and asked the secretary to find the act. +test-other/5442/41169/5442_41169_000003_000000|"He's such a blackguard! +test-other/5442/41169/5442_41169_000030_000000|"And with what he made he'd increase his stock, or buy some land for a trifle, and let it out in lots to the peasants," Levin added, smiling. He had evidently more than once come across those commercial calculations. +test-other/5484/24317/5484_24317_000040_000006|Let us hope that you will make this three leaved clover the luck promising four leaved one. +test-other/5484/24318/5484_24318_000015_000002|The blood of these innocent men would be on his head if he did not listen to her representations. +test-other/5484/24318/5484_24318_000068_000001|He was appearing before his companions only to give truth its just due. +test-other/5764/299665/5764_299665_000041_000004|He saw the seeds that man had planted wither and perish, but he sent no rain. +test-other/5764/299665/5764_299665_000070_000000|Think of the egotism of a man who believes that an infinite being wants his praise! +test-other/5764/299665/5764_299665_000102_000000|The first stone is that matter-substance-cannot be destroyed, cannot be annihilated. +test-other/5764/299665/5764_299665_000134_000000|You cannot reform these people with tracts and talk. +test-other/6070/63485/6070_63485_000025_000003|Hand me the cash, and I will hand you the pocketbook." +test-other/6070/86744/6070_86744_000027_000000|"Have you bachelor's apartments there? +test-other/6070/86745/6070_86745_000001_000002|Two windows only of the pavilion faced the street; three other windows looked into the court, and two at the back into the garden. +test-other/6128/63240/6128_63240_000012_000002|Neither five nor fifteen, and yet not ten exactly, but either nine or eleven. +test-other/6128/63240/6128_63240_000042_000002|mrs Luna explained to her sister that her freedom of speech was caused by his being a relation-though, indeed, he didn't seem to know much about them. +test-other/6128/63244/6128_63244_000002_000000|"I can't talk to those people, I can't!" said Olive Chancellor, with a face which seemed to plead for a remission of responsibility. +test-other/6432/63722/6432_63722_000026_000000|"Not the least in the world-not as much as you do," was the cool answer. +test-other/6432/63722/6432_63722_000050_000004|Queen Elizabeth was very fond of watches and clocks, and her friends, knowing that, used to present her with beautiful specimens. Some of the watches of her day were made in the form of crosses, purses, little books, and even skulls." +test-other/6432/63722/6432_63722_000080_000003|When it does it will create a sensation." +test-other/6432/63723/6432_63723_000026_000000|"No; but he will, or I'll sue him and get judgment. +test-other/6432/63723/6432_63723_000057_000000|"Then for the love of-" +test-other/6432/63723/6432_63723_000080_000000|"Hello, Harry! +test-other/6938/70848/6938_70848_000046_000003|Show me the source!" +test-other/6938/70848/6938_70848_000104_000000|With biting sarcasm he went on to speak of the Allied diplomats, till then contemptuous of Russia's invitation to an armistice, which had been accepted by the Central Powers. +test-other/7105/2330/7105_2330_000021_000000|"He won't go unless he has a brass band. +test-other/7105/2340/7105_2340_000015_000001|We feel that we must live on cream for the rest of our lives. +test-other/7902/96591/7902_96591_000008_000001|I did not come to frighten you; you frightened me." +test-other/7902/96591/7902_96591_000048_000000|"No," he thought to himself, "I don't believe they would kill me, but they would knock me about." +test-other/7902/96592/7902_96592_000024_000001|Once out of that room he could ran, and by daylight the smugglers dare not hunt him down. +test-other/7902/96592/7902_96592_000063_000000|"What for?" cried Ram. +test-other/7902/96594/7902_96594_000014_000001|These fellows are very cunning, but we shall be too many for them one of these days." +test-other/7902/96594/7902_96594_000062_000001|Keep a sharp look out on the cliff to see if Mr Raystoke is making signals for a boat. +test-other/7902/96595/7902_96595_000039_000000|The man shook his head, and stared as if he didn't half understand the drift of what was said. +test-other/7975/280057/7975_280057_000009_000000|Naturally we were Southerners in sympathy and in fact. +test-other/7975/280057/7975_280057_000025_000004|On reaching the camp the first person I saw whom I knew was Cole Younger. +test-other/7975/280076/7975_280076_000013_000001|I will give you this outline and sketch of my whereabouts and actions at the time of certain robberies with which I am charged. +test-other/7975/280084/7975_280084_000007_000000|But between the time we broke camp and the time they reached the bridge the three who went ahead drank a quart of whisky, and there was the initial blunder at Northfield. +test-other/7975/280085/7975_280085_000005_000002|Some of the boys wanted to kill him, on the theory that "dead men tell no tales," while others urged binding him and leaving him in the woods. +test-other/8131/117016/8131_117016_000005_000000|The Stonewall gang numbered perhaps five hundred. +test-other/8131/117016/8131_117016_000025_000001|"And don't let them get away!" +test-other/8131/117016/8131_117016_000047_000006|I can always go back to Earth, and I'll try to take you along. +test-other/8131/117017/8131_117017_000005_000000|Gordon hit the signal switch, and the Marspeaker let out a shrill whistle. +test-other/8131/117017/8131_117017_000020_000003|There's no graft out here." +test-other/8131/117029/8131_117029_000007_000002|Wrecks were being broken up, with salvageable material used for newer homes. Gordon came to a row of temporary bubbles, individual dwellings built like the dome, but opaque for privacy. +test-other/8131/117029/8131_117029_000023_000004|But there'll be pushers as long as weak men turn to drugs, and graft as long as voters allow the thing to get out of their hands. +test-other/8188/269288/8188_269288_000018_000000|A few moments later there came a tap at the door. +test-other/8188/269288/8188_269288_000053_000001|"Do you want to kill me? +test-other/8188/269290/8188_269290_000035_000001|"But now, Leslie, what is the trouble? +test-other/8188/269290/8188_269290_000065_000000|"I don't think she is quite well," replied Leslie. +test-other/8280/266249/8280_266249_000030_000000|The ladies were weary, and retired to their state rooms shortly after tea, but the gentlemen sought the open air again and paced the deck for some time. +test-other/8280/266249/8280_266249_000113_000000|It was the last game of cards for that trip. +test-other/8461/278226/8461_278226_000026_000000|Laura thanked the French artist and then took her husband's arm and walked away with him. +test-other/8461/281231/8461_281231_000029_000002|Before long the towering flames had surmounted every obstruction, and rose to the evening skies one huge and burning beacon, seen far and wide through the adjacent country; tower after tower crashed down, with blazing roof and rafter. diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/train-full.txt b/src/third_party/BigVGAN/filelists/LibriTTS/train-full.txt new file mode 100644 index 0000000000000000000000000000000000000000..bb67c60480dc3adfaa1bb02fbbb894a54586a38c --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/train-full.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7101f6536088ad095079582745073dc122eccea402044aeea9e09459cf2e6cc +size 52144137 diff --git a/src/third_party/BigVGAN/filelists/LibriTTS/val-full.txt b/src/third_party/BigVGAN/filelists/LibriTTS/val-full.txt new file mode 100644 index 0000000000000000000000000000000000000000..fcdf61dd6ece531ebe751ba20296dd69f9aa69c8 --- /dev/null +++ b/src/third_party/BigVGAN/filelists/LibriTTS/val-full.txt @@ -0,0 +1,119 @@ +train-clean-100/103/1241/103_1241_000000_000001|matthew Cuthbert is surprised +train-clean-100/1594/135914/1594_135914_000033_000001|He told them, that having taken refuge in a small village, he there fell sick; that some charitable peasants had taken care of him, but finding he did not recover, a camel driver had undertaken to carry him to the hospital at Bagdad. +train-clean-100/233/155990/233_155990_000018_000002|I did, however, receive aid from the Emperor of Germany. +train-clean-100/3240/131231/3240_131231_000041_000003|Some persons, thinking them to be sea fishes, placed them in salt water, according to mr Roberts. +train-clean-100/40/222/40_222_000026_000000|"No, read it yourself," cried Catherine, whose second thoughts were clearer. +train-clean-100/4406/16882/4406_16882_000014_000002|Then they set me upon a horse with my wounded child in my lap, and there being no furniture upon the horse's back, as we were going down a steep hill we both fell over the horse's head, at which they, like inhumane creatures, laughed, and rejoiced to see it, though I thought we should there have ended our days, as overcome with so many difficulties. +train-clean-100/5393/19218/5393_19218_000115_000000|"Where is it going then?" +train-clean-100/6147/34606/6147_34606_000013_000008|One was "a dancing master;" that is to say he made the rustics frisk about by pricking the calves of their legs with the point of his sword. +train-clean-100/6848/76049/6848_76049_000003_000007|But suppose she was not all ordinary female person.... +train-clean-100/7505/258964/7505_258964_000026_000007|During the Boer War horses and mules rose in price in the United States on account of British purchases. +train-clean-100/831/130739/831_130739_000015_000000|But enough of these revelations. +train-clean-100/887/123291/887_123291_000028_000000|Here the Professor laid hold of the fossil skeleton, and handled it with the skill of a dexterous showman. +train-clean-360/112/123216/112_123216_000035_000009|The wonderful day had come and Roy's violets had no place in it. +train-clean-360/1323/149236/1323_149236_000007_000004|It was vain to hope that mere words would quiet a nation which had not, in any age, been very amenable to control, and which was now agitated by hopes and resentments, such as great revolutions, following great oppressions, naturally engender. +train-clean-360/1463/134465/1463_134465_000058_000000|Both Sandy and I began to laugh. +train-clean-360/1748/1562/1748_1562_000067_000000|"Oh, Pocket, Pocket," said I; but by this time the party which had gone towards the house, rushed out again, shouting and screaming with laughter. +train-clean-360/1914/133440/1914_133440_000014_000001|With the last twenty or thirty feet of it a deadly nausea came upon me. +train-clean-360/207/143321/207_143321_000070_000002|The canoes were not on the river bank. +train-clean-360/2272/152267/2272_152267_000003_000001|After supper the knight shared his own bed with the leper. +train-clean-360/2517/135227/2517_135227_000006_000005|As I was anxious to witness some of their purely religious ceremonies, I wished to go. +train-clean-360/2709/158074/2709_158074_000054_000000|Meanwhile the women continued to protest. +train-clean-360/2929/86777/2929_86777_000009_000000|A long silence followed; the peach, like the grapes, fell to the ground. +train-clean-360/318/124224/318_124224_000022_000010|In spite of his prejudice against Edward, he could put himself into Mr Waller's place, and see the thing from his point of view. +train-clean-360/3368/170952/3368_170952_000006_000000|And can he be fearless of death, or will he choose death in battle rather than defeat and slavery, who believes the world below to be real and terrible? +train-clean-360/3549/9203/3549_9203_000005_000004|We must hope so. There are examples. +train-clean-360/3835/178028/3835_178028_000007_000001|That day Prince Vasili no longer boasted of his protege Kutuzov, but remained silent when the commander in chief was mentioned. +train-clean-360/3994/149798/3994_149798_000005_000002|Afterward we can visit the mountain and punish the cruel magician of the Flatheads." +train-clean-360/4257/6397/4257_6397_000009_000000|At that time Nostromo had been already long enough in the country to raise to the highest pitch Captain Mitchell's opinion of the extraordinary value of his discovery. +train-clean-360/454/134728/454_134728_000133_000000|After a week of physical anguish, Unrest and pain, and feverish heat, Toward the ending day a calm and lull comes on, Three hours of peace and soothing rest of brain. +train-clean-360/4848/28247/4848_28247_000026_000002|Had he gained this arduous height only to behold the rocks carpeted with ice and snow, and reaching interminably to the far off horizon? +train-clean-360/5039/1189/5039_1189_000091_000000|The Shaggy Man sat down again and seemed well pleased. +train-clean-360/5261/19373/5261_19373_000011_000001|Some cause was evidently at work on this distant planet, causing it to disagree with its motion as calculated according to the law of gravitation. +train-clean-360/5538/70919/5538_70919_000032_000001|Only one person in the world could have laid those discoloured pearls at his door in the dead of night. The black figure in the garden, with the chiffon fluttering about its head, was Evelina Grey-or what was left of her. +train-clean-360/5712/48848/5712_48848_000060_000003|Lily for the time had been raised to a pinnacle,--a pinnacle which might be dangerous, but which was, at any rate, lofty. +train-clean-360/5935/43322/5935_43322_000050_000002|I think too-yes, I think that on the whole the ritual is impressive. +train-clean-360/6115/58433/6115_58433_000007_000002|We must run the risk." +train-clean-360/6341/64956/6341_64956_000040_000000|"Why, papa, I thought we were going to have such a nice time, and she just spoiled it all." +train-clean-360/6509/67147/6509_67147_000028_000003|It "was n't done" in England. +train-clean-360/6694/70837/6694_70837_000027_000002|There an enormous smiling sailor stopped me, and when I showed my pass, just said, "If you were Saint Michael himself, comrade, you couldn't pass here!" Through the glass of the door I made out the distorted face and gesticulating arms of a French correspondent, locked in.... +train-clean-360/6956/76046/6956_76046_000055_000001|Twelve hundred, fifteen hundred millions perhaps." +train-clean-360/7145/87280/7145_87280_000004_000003|This modern Ulysses made a masterful effort, but alas! had no ships to carry him away, and no wax with which to fill his ears. +train-clean-360/7314/77782/7314_77782_000011_000000|"Well, then, what in thunder is the matter with you?" cried the Lawyer, irritated. +train-clean-360/7525/92915/7525_92915_000034_000001|It was desperate, too, and lasted nearly all day-and it was one of the important battles of the world, although the numbers engaged in it were not large. +train-clean-360/7754/108640/7754_108640_000001_000004|Was I aware-was I fully aware of the discrepancy between us? +train-clean-360/7909/106369/7909_106369_000006_000002|And Colchian Aea lies at the edge of Pontus and of the world." +train-clean-360/8011/280922/8011_280922_000009_000000|He stretched out his hand, and all at once stroked my cheek. +train-clean-360/8176/115046/8176_115046_000027_000001|"Bless my soul, I never can understand it!" +train-clean-360/8459/292347/8459_292347_000015_000000|A woman near Gort, in Galway, says: 'There is a boy, now, of the Cloran's; but I wouldn't for the world let them think I spoke of him; it's two years since he came from America, and since that time he never went to Mass, or to church, or to fairs, or to market, or to stand on the cross roads, or to hurling, or to nothing. +train-clean-360/8699/291107/8699_291107_000003_000005|He leaned closer over it, regardless of the thin choking haze that spread about his face. In his attitude there was a rigidity of controlled excitement out of keeping with the seeming harmlessness of the experiment. +train-clean-360/8855/283242/8855_283242_000061_000000|"That couldn't be helped, grannie. +train-other-500/102/129232/102_129232_000050_000000|Is it otherwise in the newest romance? +train-other-500/1124/134775/1124_134775_000087_000001|Some of them are enclosed only by hedges, which lends a cheerful aspect to the street. +train-other-500/1239/138254/1239_138254_000010_000001|It was past twelve when all preparations were finished. +train-other-500/1373/132103/1373_132103_000056_000000|So they moved on. +train-other-500/1566/153036/1566_153036_000087_000003|You enter the river close by the trees, and then keep straight for the pile of stones, which is some fifty yards higher up, for the ford crosses the river at an angle." +train-other-500/1653/142352/1653_142352_000005_000002|If he should not come! +train-other-500/1710/133294/1710_133294_000023_000000|When the Indians were the sole inhabitants of the wilds from whence they have since been expelled, their wants were few. +train-other-500/1773/139602/1773_139602_000032_000001|When the rabbit saw that the badger was getting well, he thought of another plan by which he could compass the creature's death. +train-other-500/1920/1793/1920_1793_000037_000001|She has a little Blenheim lapdog, that she loves a thousand times more than she ever will me!" +train-other-500/2067/143535/2067_143535_000009_000002|Indeed, there, to the left, was a stone shelf with a little ledge to it three inches or so high, and on the shelf lay what I took to be a corpse; at any rate, it looked like one, with something white thrown over it. +train-other-500/2208/11020/2208_11020_000037_000001|It's at my place over there.' +train-other-500/2312/157868/2312_157868_000019_000002|I am the manager of the theatre, and I'm thundering glad that your first play has been produced at the 'New York,' sir. +train-other-500/2485/151992/2485_151992_000028_000005|At last he looked up at his wife and said, in a gentle tone: +train-other-500/2587/54186/2587_54186_000015_000000|Concerning the work as a whole he wrote to Clara while in the throes of composition: "This music now in me, and always such beautiful melodies! +train-other-500/2740/288813/2740_288813_000018_000003|But Philip had kept him apart, had banked him off, and yet drained him to the dregs. +train-other-500/2943/171001/2943_171001_000122_000000|The sound of his voice pronouncing her name aroused her. +train-other-500/3063/138651/3063_138651_000028_000000|But, as may be imagined, the unfortunate john was as much surprised by this rencounter as the other two. +train-other-500/3172/166439/3172_166439_000050_000000|And now at last was clear a thing that had puzzled greatly-the mechanism of that opening process by which sphere became oval disk, pyramid a four pointed star and-as I had glimpsed in the play of the Little Things about Norhala, could see now so plainly in the Keeper-the blocks took this inverted cruciform shape. +train-other-500/331/132019/331_132019_000038_000000|"I say, this is folly! +train-other-500/3467/166570/3467_166570_000054_000001|Does he never mention Orlando?" +train-other-500/3587/140711/3587_140711_000015_000001|O fie, mrs Jervis, said I, how could you serve me so? Besides, it looks too free both in me, and to him. +train-other-500/3675/187020/3675_187020_000026_000001|"I wonder what would be suitable? +train-other-500/3819/134146/3819_134146_000019_000001|Also the figure half hidden by the cupboard door-was a female figure, massive, and in flowing robes. +train-other-500/3912/77626/3912_77626_000003_000004|You may almost distinguish the figures on the clock that has just told the hour. +train-other-500/4015/63729/4015_63729_000058_000000|"It does." +train-other-500/413/22436/413_22436_000035_000003|I conjecture, the French squadron is bound for Malta and Alexandria, and the Spanish fleet for the attack of Minorca." +train-other-500/4218/41159/4218_41159_000028_000002|Yes? That worries Alexey. +train-other-500/4352/10940/4352_10940_000037_000002|He doesn't exist." +train-other-500/4463/26871/4463_26871_000023_000000|"I did not notice him following me," she said timidly. +train-other-500/4591/14356/4591_14356_000019_000000|"Within three days," cried the enchanter, loudly, "bring Rinaldo and Ricciardetto into the pass of Ronces Valles. +train-other-500/4738/291957/4738_291957_000000_000001|ODE ON THE SPRING. +train-other-500/4824/36029/4824_36029_000045_000003|And indeed Janet herself had taken no part in the politics, content merely to advise the combatants upon their demeanour. +train-other-500/4936/65528/4936_65528_000014_000007|I immediately responded, "Yes, they are most terrible struck on each other," and I said it in a tone that indicated I thought it a most beautiful and lovely thing that they should be so. +train-other-500/5019/38670/5019_38670_000017_000000|"Let me make you a present of the gloves," she said, with her irresistible smile. +train-other-500/5132/33409/5132_33409_000016_000001|They waited on the table in Valhalla. +train-other-500/52/121057/52_121057_000019_000000|"I," cried the steward with a strange expression. +train-other-500/5321/53046/5321_53046_000025_000003|I gather from what mrs joel said that she's rather touched in her mind too, and has an awful hankering to get home here-to this very house. +train-other-500/5429/210770/5429_210770_000029_000006|But this was not all. +train-other-500/557/129797/557_129797_000072_000001|The guns were manned, the gunners already kindling fuses, when the buccaneer fleet, whilst still heading for Palomas, was observed to bear away to the west. +train-other-500/572/128861/572_128861_000016_000002|My home was desolate. +train-other-500/5826/53497/5826_53497_000044_000001|If it be as you say, he will have shown himself noble, and his nobility will have consisted in this, that he has been willing to take that which he does not want, in order that he may succour one whom he loves. +train-other-500/5906/52158/5906_52158_000055_000000|The impression that he gets this knowledge or suspicion from the outside is due, the scientists say, to the fact that his thinking has proceeded at such lightning like speed that he was unable to watch the wheels go round. +train-other-500/6009/57639/6009_57639_000038_000000|This, friendly reader, is my only motive. +train-other-500/6106/58196/6106_58196_000007_000001|I tell you that you must make the dress. +train-other-500/6178/86034/6178_86034_000079_000004|Then she will grow calmer, and will know you again. +train-other-500/6284/63091/6284_63091_000133_000001|I don't want to go anywhere where anybody'll see me." +train-other-500/6436/104980/6436_104980_000009_000002|I guess you never heard about this house." +train-other-500/6540/232291/6540_232291_000017_000003|The girl was not wholly a savage. +train-other-500/6627/67844/6627_67844_000046_000002|The other girls had stopped talking, and now looked at Sylvia as if wondering what she would say. +train-other-500/6707/77351/6707_77351_000002_000006|But our first words I may give you, because though they conveyed nothing to me at the time, afterwards they meant much. +train-other-500/6777/76694/6777_76694_000013_000011|When they are forcibly put out of Garraway's on Saturday night-which they must be, for they never would go out of their own accord-where do they vanish until Monday morning? +train-other-500/690/133452/690_133452_000011_000000|Campany lifted his quill pen and pointed to a case of big leather bound volumes in a far corner of the room. +train-other-500/7008/34667/7008_34667_000032_000002|What had happened? +train-other-500/7131/92815/7131_92815_000039_000001|The cabman tried to pass to the left, but a heavy express wagon cut him off. +train-other-500/7220/77911/7220_77911_000005_000000|"Do? +train-other-500/7326/245693/7326_245693_000008_000000|Whether the Appetite Is a Special Power of the Soul? +train-other-500/7392/105672/7392_105672_000013_000005|Whoever, being required, refused to answer upon oath to any article of this act of settlement, was declared to be guilty of treason; and by this clause a species of political inquisition was established in the kingdom, as well as the accusations of treason multiplied to an unreasonable degree. +train-other-500/7512/98636/7512_98636_000017_000002|A man thus rarely makes provision for the future, and looks with scorn on foreign customs which seem to betoken a fear lest, in old age, ungrateful children may neglect their parents and cast them aside. +train-other-500/7654/258963/7654_258963_000007_000007|Egypt, for a time reduced to a semi desert condition, has only in the past century been restored to a certain extent by the use of new methods and a return to the old ones. +train-other-500/7769/99396/7769_99396_000020_000002|I had to go out once a day in search of food. +train-other-500/791/127519/791_127519_000086_000000|This was how it came about. +train-other-500/8042/113769/8042_113769_000021_000000|House the second. +train-other-500/8180/274725/8180_274725_000010_000000|"What fools men are in love matters," quoth Patty to herself-"at least most men!" with a thought backward to Mark's sensible choosing. +train-other-500/8291/282929/8291_282929_000031_000006|He's in a devil of a-Well, he needs the money, and I've got to get it for him. You know my word's good, Cooper." +train-other-500/8389/120181/8389_120181_000022_000000|"No," I answered. +train-other-500/8476/269293/8476_269293_000078_000001|Annie, in some wonder, went downstairs alone. +train-other-500/8675/295195/8675_295195_000004_000004|Everything had gone on prosperously with them, and they had reared many successive families of young Nutcrackers, who went forth to assume their places in the forest of life, and to reflect credit on their bringing up,--so that naturally enough they began to have a very easy way of considering themselves models of wisdom. +train-other-500/9000/282381/9000_282381_000016_000008|Bank facings seemed to indicate that the richest pay dirt lay at bed rock. +train-other-500/978/132494/978_132494_000017_000001|And what made you come at that very minute? diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_1 b/src/third_party/BigVGAN/incl_licenses/LICENSE_1 new file mode 100644 index 0000000000000000000000000000000000000000..774eaf8462a160c855fb01f069b3eb8164ce019c --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_1 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jungil Kong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_2 b/src/third_party/BigVGAN/incl_licenses/LICENSE_2 new file mode 100644 index 0000000000000000000000000000000000000000..522989138d63bbdee9049e19d0cd81751f818c8a --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_2 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Edward Dixon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_3 b/src/third_party/BigVGAN/incl_licenses/LICENSE_3 new file mode 100644 index 0000000000000000000000000000000000000000..eeac88fb9dc15a1427b41173cf5f136327230c49 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_3 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_4 b/src/third_party/BigVGAN/incl_licenses/LICENSE_4 new file mode 100644 index 0000000000000000000000000000000000000000..f87b31c7f648a6863dbda41e3ef94aebcd5d6332 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_4 @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Seungwon Park 박승원 +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_5 b/src/third_party/BigVGAN/incl_licenses/LICENSE_5 new file mode 100644 index 0000000000000000000000000000000000000000..86ac396a9469809d46011859baeb1d34346ab9b5 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_5 @@ -0,0 +1,16 @@ +Copyright 2020 Alexandre Défossez + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_6 b/src/third_party/BigVGAN/incl_licenses/LICENSE_6 new file mode 100644 index 0000000000000000000000000000000000000000..e206724f34e848226ac3aee3ccd8afdade11f345 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_6 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present, Descript + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_7 b/src/third_party/BigVGAN/incl_licenses/LICENSE_7 new file mode 100644 index 0000000000000000000000000000000000000000..6ee845091a4e0a1569c45ce1bdd47170f674d476 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_7 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charactr Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/incl_licenses/LICENSE_8 b/src/third_party/BigVGAN/incl_licenses/LICENSE_8 new file mode 100644 index 0000000000000000000000000000000000000000..188a7c077e81918487a546520a57f9923aef3e25 --- /dev/null +++ b/src/third_party/BigVGAN/incl_licenses/LICENSE_8 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Amphion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/third_party/BigVGAN/inference.py b/src/third_party/BigVGAN/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..4a2f02e09263c7426cbc1179c60732b23696869d --- /dev/null +++ b/src/third_party/BigVGAN/inference.py @@ -0,0 +1,89 @@ +# Adapted from https://github.com/jik876/hifi-gan under the MIT license. +# LICENSE is in incl_licenses directory. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import os +import argparse +import json +import torch +import librosa +from utils import load_checkpoint +from meldataset import get_mel_spectrogram +from scipy.io.wavfile import write +from env import AttrDict +from meldataset import MAX_WAV_VALUE +from bigvgan import BigVGAN as Generator + +h = None +device = None +torch.backends.cudnn.benchmark = False + + +def inference(a, h): + generator = Generator(h, use_cuda_kernel=a.use_cuda_kernel).to(device) + + state_dict_g = load_checkpoint(a.checkpoint_file, device) + generator.load_state_dict(state_dict_g["generator"]) + + filelist = os.listdir(a.input_wavs_dir) + + os.makedirs(a.output_dir, exist_ok=True) + + generator.eval() + generator.remove_weight_norm() + with torch.no_grad(): + for i, filname in enumerate(filelist): + # Load the ground truth audio and resample if necessary + wav, sr = librosa.load( + os.path.join(a.input_wavs_dir, filname), sr=h.sampling_rate, mono=True + ) + wav = torch.FloatTensor(wav).to(device) + # Compute mel spectrogram from the ground truth audio + x = get_mel_spectrogram(wav.unsqueeze(0), generator.h) + + y_g_hat = generator(x) + + audio = y_g_hat.squeeze() + audio = audio * MAX_WAV_VALUE + audio = audio.cpu().numpy().astype("int16") + + output_file = os.path.join( + a.output_dir, os.path.splitext(filname)[0] + "_generated.wav" + ) + write(output_file, h.sampling_rate, audio) + print(output_file) + + +def main(): + print("Initializing Inference Process..") + + parser = argparse.ArgumentParser() + parser.add_argument("--input_wavs_dir", default="test_files") + parser.add_argument("--output_dir", default="generated_files") + parser.add_argument("--checkpoint_file", required=True) + parser.add_argument("--use_cuda_kernel", action="store_true", default=False) + + a = parser.parse_args() + + config_file = os.path.join(os.path.split(a.checkpoint_file)[0], "config.json") + with open(config_file) as f: + data = f.read() + + global h + json_config = json.loads(data) + h = AttrDict(json_config) + + torch.manual_seed(h.seed) + global device + if torch.cuda.is_available(): + torch.cuda.manual_seed(h.seed) + device = torch.device("cuda") + else: + device = torch.device("cpu") + + inference(a, h) + + +if __name__ == "__main__": + main() diff --git a/src/third_party/BigVGAN/inference_e2e.py b/src/third_party/BigVGAN/inference_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..94c8ebe7a20c3edee6a58cd3a846bde8f42f0d46 --- /dev/null +++ b/src/third_party/BigVGAN/inference_e2e.py @@ -0,0 +1,102 @@ +# Adapted from https://github.com/jik876/hifi-gan under the MIT license. +# LICENSE is in incl_licenses directory. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import glob +import os +import numpy as np +import argparse +import json +import torch +from scipy.io.wavfile import write +from env import AttrDict +from meldataset import MAX_WAV_VALUE +from bigvgan import BigVGAN as Generator + +h = None +device = None +torch.backends.cudnn.benchmark = False + + +def load_checkpoint(filepath, device): + assert os.path.isfile(filepath) + print(f"Loading '{filepath}'") + checkpoint_dict = torch.load(filepath, map_location=device) + print("Complete.") + return checkpoint_dict + + +def scan_checkpoint(cp_dir, prefix): + pattern = os.path.join(cp_dir, prefix + "*") + cp_list = glob.glob(pattern) + if len(cp_list) == 0: + return "" + return sorted(cp_list)[-1] + + +def inference(a, h): + generator = Generator(h, use_cuda_kernel=a.use_cuda_kernel).to(device) + + state_dict_g = load_checkpoint(a.checkpoint_file, device) + generator.load_state_dict(state_dict_g["generator"]) + + filelist = os.listdir(a.input_mels_dir) + + os.makedirs(a.output_dir, exist_ok=True) + + generator.eval() + generator.remove_weight_norm() + with torch.no_grad(): + for i, filname in enumerate(filelist): + # Load the mel spectrogram in .npy format + x = np.load(os.path.join(a.input_mels_dir, filname)) + x = torch.FloatTensor(x).to(device) + if len(x.shape) == 2: + x = x.unsqueeze(0) + + y_g_hat = generator(x) + + audio = y_g_hat.squeeze() + audio = audio * MAX_WAV_VALUE + audio = audio.cpu().numpy().astype("int16") + + output_file = os.path.join( + a.output_dir, os.path.splitext(filname)[0] + "_generated_e2e.wav" + ) + write(output_file, h.sampling_rate, audio) + print(output_file) + + +def main(): + print("Initializing Inference Process..") + + parser = argparse.ArgumentParser() + parser.add_argument("--input_mels_dir", default="test_mel_files") + parser.add_argument("--output_dir", default="generated_files_from_mel") + parser.add_argument("--checkpoint_file", required=True) + parser.add_argument("--use_cuda_kernel", action="store_true", default=False) + + a = parser.parse_args() + + config_file = os.path.join(os.path.split(a.checkpoint_file)[0], "config.json") + with open(config_file) as f: + data = f.read() + + global h + json_config = json.loads(data) + h = AttrDict(json_config) + + torch.manual_seed(h.seed) + global device + if torch.cuda.is_available(): + torch.cuda.manual_seed(h.seed) + device = torch.device("cuda") + else: + device = torch.device("cpu") + + inference(a, h) + + +if __name__ == "__main__": + main() diff --git a/src/third_party/BigVGAN/loss.py b/src/third_party/BigVGAN/loss.py new file mode 100644 index 0000000000000000000000000000000000000000..3fdfba26c66bd05566c4ba7946d2f1163eed2067 --- /dev/null +++ b/src/third_party/BigVGAN/loss.py @@ -0,0 +1,254 @@ +# Copyright (c) 2024 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 torch +import torch.nn.functional as F +import torch.nn as nn +from librosa.filters import mel as librosa_mel_fn +from scipy import signal + +import typing +from typing import Optional, List, Union, Dict, Tuple +from collections import namedtuple +import math +import functools + + +# Adapted from https://github.com/descriptinc/descript-audio-codec/blob/main/dac/nn/loss.py under the MIT license. +# LICENSE is in incl_licenses directory. +class MultiScaleMelSpectrogramLoss(nn.Module): + """Compute distance between mel spectrograms. Can be used + in a multi-scale way. + + Parameters + ---------- + n_mels : List[int] + Number of mels per STFT, by default [5, 10, 20, 40, 80, 160, 320], + window_lengths : List[int], optional + Length of each window of each STFT, by default [32, 64, 128, 256, 512, 1024, 2048] + loss_fn : typing.Callable, optional + How to compare each loss, by default nn.L1Loss() + clamp_eps : float, optional + Clamp on the log magnitude, below, by default 1e-5 + mag_weight : float, optional + Weight of raw magnitude portion of loss, by default 0.0 (no ampliciation on mag part) + log_weight : float, optional + Weight of log magnitude portion of loss, by default 1.0 + pow : float, optional + Power to raise magnitude to before taking log, by default 1.0 + weight : float, optional + Weight of this loss, by default 1.0 + match_stride : bool, optional + Whether to match the stride of convolutional layers, by default False + + Implementation copied from: https://github.com/descriptinc/lyrebird-audiotools/blob/961786aa1a9d628cca0c0486e5885a457fe70c1a/audiotools/metrics/spectral.py + Additional code copied and modified from https://github.com/descriptinc/audiotools/blob/master/audiotools/core/audio_signal.py + """ + + def __init__( + self, + sampling_rate: int, + n_mels: List[int] = [5, 10, 20, 40, 80, 160, 320], + window_lengths: List[int] = [32, 64, 128, 256, 512, 1024, 2048], + loss_fn: typing.Callable = nn.L1Loss(), + clamp_eps: float = 1e-5, + mag_weight: float = 0.0, + log_weight: float = 1.0, + pow: float = 1.0, + weight: float = 1.0, + match_stride: bool = False, + mel_fmin: List[float] = [0, 0, 0, 0, 0, 0, 0], + mel_fmax: List[float] = [None, None, None, None, None, None, None], + window_type: str = "hann", + ): + super().__init__() + self.sampling_rate = sampling_rate + + STFTParams = namedtuple( + "STFTParams", + ["window_length", "hop_length", "window_type", "match_stride"], + ) + + self.stft_params = [ + STFTParams( + window_length=w, + hop_length=w // 4, + match_stride=match_stride, + window_type=window_type, + ) + for w in window_lengths + ] + self.n_mels = n_mels + self.loss_fn = loss_fn + self.clamp_eps = clamp_eps + self.log_weight = log_weight + self.mag_weight = mag_weight + self.weight = weight + self.mel_fmin = mel_fmin + self.mel_fmax = mel_fmax + self.pow = pow + + @staticmethod + @functools.lru_cache(None) + def get_window( + window_type, + window_length, + ): + return signal.get_window(window_type, window_length) + + @staticmethod + @functools.lru_cache(None) + def get_mel_filters(sr, n_fft, n_mels, fmin, fmax): + return librosa_mel_fn(sr=sr, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax) + + def mel_spectrogram( + self, + wav, + n_mels, + fmin, + fmax, + window_length, + hop_length, + match_stride, + window_type, + ): + """ + Mirrors AudioSignal.mel_spectrogram used by BigVGAN-v2 training from: + https://github.com/descriptinc/audiotools/blob/master/audiotools/core/audio_signal.py + """ + B, C, T = wav.shape + + if match_stride: + assert ( + hop_length == window_length // 4 + ), "For match_stride, hop must equal n_fft // 4" + right_pad = math.ceil(T / hop_length) * hop_length - T + pad = (window_length - hop_length) // 2 + else: + right_pad = 0 + pad = 0 + + wav = torch.nn.functional.pad(wav, (pad, pad + right_pad), mode="reflect") + + window = self.get_window(window_type, window_length) + window = torch.from_numpy(window).to(wav.device).float() + + stft = torch.stft( + wav.reshape(-1, T), + n_fft=window_length, + hop_length=hop_length, + window=window, + return_complex=True, + center=True, + ) + _, nf, nt = stft.shape + stft = stft.reshape(B, C, nf, nt) + if match_stride: + """ + Drop first two and last two frames, which are added, because of padding. Now num_frames * hop_length = num_samples. + """ + stft = stft[..., 2:-2] + magnitude = torch.abs(stft) + + nf = magnitude.shape[2] + mel_basis = self.get_mel_filters( + self.sampling_rate, 2 * (nf - 1), n_mels, fmin, fmax + ) + mel_basis = torch.from_numpy(mel_basis).to(wav.device) + mel_spectrogram = magnitude.transpose(2, -1) @ mel_basis.T + mel_spectrogram = mel_spectrogram.transpose(-1, 2) + + return mel_spectrogram + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + """Computes mel loss between an estimate and a reference + signal. + + Parameters + ---------- + x : torch.Tensor + Estimate signal + y : torch.Tensor + Reference signal + + Returns + ------- + torch.Tensor + Mel loss. + """ + + loss = 0.0 + for n_mels, fmin, fmax, s in zip( + self.n_mels, self.mel_fmin, self.mel_fmax, self.stft_params + ): + kwargs = { + "n_mels": n_mels, + "fmin": fmin, + "fmax": fmax, + "window_length": s.window_length, + "hop_length": s.hop_length, + "match_stride": s.match_stride, + "window_type": s.window_type, + } + + x_mels = self.mel_spectrogram(x, **kwargs) + y_mels = self.mel_spectrogram(y, **kwargs) + x_logmels = torch.log( + x_mels.clamp(min=self.clamp_eps).pow(self.pow) + ) / torch.log(torch.tensor(10.0)) + y_logmels = torch.log( + y_mels.clamp(min=self.clamp_eps).pow(self.pow) + ) / torch.log(torch.tensor(10.0)) + + loss += self.log_weight * self.loss_fn(x_logmels, y_logmels) + loss += self.mag_weight * self.loss_fn(x_logmels, y_logmels) + + return loss + + +# Loss functions +def feature_loss( + fmap_r: List[List[torch.Tensor]], fmap_g: List[List[torch.Tensor]] +) -> torch.Tensor: + + 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 # This equates to lambda=2.0 for the feature matching loss + + +def discriminator_loss( + disc_real_outputs: List[torch.Tensor], disc_generated_outputs: List[torch.Tensor] +) -> Tuple[torch.Tensor, List[torch.Tensor], List[torch.Tensor]]: + + 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: List[torch.Tensor], +) -> Tuple[torch.Tensor, List[torch.Tensor]]: + + 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 diff --git a/src/third_party/BigVGAN/meldataset.py b/src/third_party/BigVGAN/meldataset.py new file mode 100644 index 0000000000000000000000000000000000000000..9661bb2f5e04835f09b1d6e318e3360094d187b4 --- /dev/null +++ b/src/third_party/BigVGAN/meldataset.py @@ -0,0 +1,396 @@ +# Copyright (c) 2024 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 math +import os +import random +import torch +import torch.utils.data +import numpy as np +import librosa +from librosa.filters import mel as librosa_mel_fn +import pathlib +from tqdm import tqdm +from typing import List, Tuple, Optional +from env import AttrDict + +MAX_WAV_VALUE = 32767.0 # NOTE: 32768.0 -1 to prevent int16 overflow (results in popping sound in corner cases) + + +def dynamic_range_compression(x, C=1, clip_val=1e-5): + return np.log(np.clip(x, a_min=clip_val, a_max=None) * C) + + +def dynamic_range_decompression(x, C=1): + return np.exp(x) / C + + +def dynamic_range_compression_torch(x, C=1, clip_val=1e-5): + return torch.log(torch.clamp(x, min=clip_val) * C) + + +def dynamic_range_decompression_torch(x, C=1): + return torch.exp(x) / C + + +def spectral_normalize_torch(magnitudes): + return dynamic_range_compression_torch(magnitudes) + + +def spectral_de_normalize_torch(magnitudes): + return dynamic_range_decompression_torch(magnitudes) + + +mel_basis_cache = {} +hann_window_cache = {} + + +def mel_spectrogram( + y: torch.Tensor, + n_fft: int, + num_mels: int, + sampling_rate: int, + hop_size: int, + win_size: int, + fmin: int, + fmax: int = None, + center: bool = False, +) -> torch.Tensor: + """ + Calculate the mel spectrogram of an input signal. + This function uses slaney norm for the librosa mel filterbank (using librosa.filters.mel) and uses Hann window for STFT (using torch.stft). + + Args: + y (torch.Tensor): Input signal. + n_fft (int): FFT size. + num_mels (int): Number of mel bins. + sampling_rate (int): Sampling rate of the input signal. + hop_size (int): Hop size for STFT. + win_size (int): Window size for STFT. + fmin (int): Minimum frequency for mel filterbank. + fmax (int): Maximum frequency for mel filterbank. If None, defaults to half the sampling rate (fmax = sr / 2.0) inside librosa_mel_fn + center (bool): Whether to pad the input to center the frames. Default is False. + + Returns: + torch.Tensor: Mel spectrogram. + """ + if torch.min(y) < -1.0: + print(f"[WARNING] Min value of input waveform signal is {torch.min(y)}") + if torch.max(y) > 1.0: + print(f"[WARNING] Max value of input waveform signal is {torch.max(y)}") + + device = y.device + key = f"{n_fft}_{num_mels}_{sampling_rate}_{hop_size}_{win_size}_{fmin}_{fmax}_{device}" + + if key not in mel_basis_cache: + mel = librosa_mel_fn( + sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax + ) + mel_basis_cache[key] = torch.from_numpy(mel).float().to(device) + hann_window_cache[key] = torch.hann_window(win_size).to(device) + + mel_basis = mel_basis_cache[key] + hann_window = hann_window_cache[key] + + padding = (n_fft - hop_size) // 2 + y = torch.nn.functional.pad( + y.unsqueeze(1), (padding, padding), mode="reflect" + ).squeeze(1) + + spec = torch.stft( + y, + n_fft, + hop_length=hop_size, + win_length=win_size, + window=hann_window, + center=center, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + spec = torch.sqrt(torch.view_as_real(spec).pow(2).sum(-1) + 1e-9) + + mel_spec = torch.matmul(mel_basis, spec) + mel_spec = spectral_normalize_torch(mel_spec) + + return mel_spec + + +def get_mel_spectrogram(wav, h): + """ + Generate mel spectrogram from a waveform using given hyperparameters. + + Args: + wav (torch.Tensor): Input waveform. + h: Hyperparameters object with attributes n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax. + + Returns: + torch.Tensor: Mel spectrogram. + """ + return mel_spectrogram( + wav, + h.n_fft, + h.num_mels, + h.sampling_rate, + h.hop_size, + h.win_size, + h.fmin, + h.fmax, + ) + + +def get_dataset_filelist(a): + training_files = [] + validation_files = [] + list_unseen_validation_files = [] + + with open(a.input_training_file, "r", encoding="utf-8") as fi: + training_files = [ + os.path.join(a.input_wavs_dir, x.split("|")[0] + ".wav") + for x in fi.read().split("\n") + if len(x) > 0 + ] + print(f"first training file: {training_files[0]}") + + with open(a.input_validation_file, "r", encoding="utf-8") as fi: + validation_files = [ + os.path.join(a.input_wavs_dir, x.split("|")[0] + ".wav") + for x in fi.read().split("\n") + if len(x) > 0 + ] + print(f"first validation file: {validation_files[0]}") + + for i in range(len(a.list_input_unseen_validation_file)): + with open(a.list_input_unseen_validation_file[i], "r", encoding="utf-8") as fi: + unseen_validation_files = [ + os.path.join(a.list_input_unseen_wavs_dir[i], x.split("|")[0] + ".wav") + for x in fi.read().split("\n") + if len(x) > 0 + ] + print( + f"first unseen {i}th validation fileset: {unseen_validation_files[0]}" + ) + list_unseen_validation_files.append(unseen_validation_files) + + return training_files, validation_files, list_unseen_validation_files + + +class MelDataset(torch.utils.data.Dataset): + def __init__( + self, + training_files: List[str], + hparams: AttrDict, + segment_size: int, + n_fft: int, + num_mels: int, + hop_size: int, + win_size: int, + sampling_rate: int, + fmin: int, + fmax: Optional[int], + split: bool = True, + shuffle: bool = True, + device: str = None, + fmax_loss: Optional[int] = None, + fine_tuning: bool = False, + base_mels_path: str = None, + is_seen: bool = True, + ): + self.audio_files = training_files + random.seed(1234) + if shuffle: + random.shuffle(self.audio_files) + self.hparams = hparams + self.is_seen = is_seen + if self.is_seen: + self.name = pathlib.Path(self.audio_files[0]).parts[0] + else: + self.name = "-".join(pathlib.Path(self.audio_files[0]).parts[:2]).strip("/") + + self.segment_size = segment_size + self.sampling_rate = sampling_rate + self.split = split + self.n_fft = n_fft + self.num_mels = num_mels + self.hop_size = hop_size + self.win_size = win_size + self.fmin = fmin + self.fmax = fmax + self.fmax_loss = fmax_loss + self.device = device + self.fine_tuning = fine_tuning + self.base_mels_path = base_mels_path + + print("[INFO] checking dataset integrity...") + for i in tqdm(range(len(self.audio_files))): + assert os.path.exists( + self.audio_files[i] + ), f"{self.audio_files[i]} not found" + + def __getitem__( + self, index: int + ) -> Tuple[torch.Tensor, torch.Tensor, str, torch.Tensor]: + try: + filename = self.audio_files[index] + + # Use librosa.load that ensures loading waveform into mono with [-1, 1] float values + # Audio is ndarray with shape [T_time]. Disable auto-resampling here to minimize overhead + # The on-the-fly resampling during training will be done only for the obtained random chunk + audio, source_sampling_rate = librosa.load(filename, sr=None, mono=True) + + # Main logic that uses pair for training BigVGAN + if not self.fine_tuning: + if self.split: # Training step + # Obtain randomized audio chunk + if source_sampling_rate != self.sampling_rate: + # Adjust segment size to crop if the source sr is different + target_segment_size = math.ceil( + self.segment_size + * (source_sampling_rate / self.sampling_rate) + ) + else: + target_segment_size = self.segment_size + + # Compute upper bound index for the random chunk + random_chunk_upper_bound = max( + 0, audio.shape[0] - target_segment_size + ) + + # Crop or pad audio to obtain random chunk with target_segment_size + if audio.shape[0] >= target_segment_size: + audio_start = random.randint(0, random_chunk_upper_bound) + audio = audio[audio_start : audio_start + target_segment_size] + else: + audio = np.pad( + audio, + (0, target_segment_size - audio.shape[0]), + mode="constant", + ) + + # Resample audio chunk to self.sampling rate + if source_sampling_rate != self.sampling_rate: + audio = librosa.resample( + audio, + orig_sr=source_sampling_rate, + target_sr=self.sampling_rate, + ) + if audio.shape[0] > self.segment_size: + # trim last elements to match self.segment_size (e.g., 16385 for 44khz downsampled to 24khz -> 16384) + audio = audio[: self.segment_size] + + else: # Validation step + # Resample full audio clip to target sampling rate + if source_sampling_rate != self.sampling_rate: + audio = librosa.resample( + audio, + orig_sr=source_sampling_rate, + target_sr=self.sampling_rate, + ) + # Trim last elements to match audio length to self.hop_size * n for evaluation + if (audio.shape[0] % self.hop_size) != 0: + audio = audio[: -(audio.shape[0] % self.hop_size)] + + # BigVGAN is trained using volume-normalized waveform + audio = librosa.util.normalize(audio) * 0.95 + + # Cast ndarray to torch tensor + audio = torch.FloatTensor(audio) + audio = audio.unsqueeze(0) # [B(1), self.segment_size] + + # Compute mel spectrogram corresponding to audio + mel = mel_spectrogram( + audio, + self.n_fft, + self.num_mels, + self.sampling_rate, + self.hop_size, + self.win_size, + self.fmin, + self.fmax, + center=False, + ) # [B(1), self.num_mels, self.segment_size // self.hop_size] + + # Fine-tuning logic that uses pre-computed mel. Example: Using TTS model-generated mel as input + else: + # For fine-tuning, assert that the waveform is in the defined sampling_rate + # Fine-tuning won't support on-the-fly resampling to be fool-proof (the dataset should have been prepared properly) + assert ( + source_sampling_rate == self.sampling_rate + ), f"For fine_tuning, waveform must be in the spcified sampling rate {self.sampling_rate}, got {source_sampling_rate}" + + # Cast ndarray to torch tensor + audio = torch.FloatTensor(audio) + audio = audio.unsqueeze(0) # [B(1), T_time] + + # Load pre-computed mel from disk + mel = np.load( + os.path.join( + self.base_mels_path, + os.path.splitext(os.path.split(filename)[-1])[0] + ".npy", + ) + ) + mel = torch.from_numpy(mel) + + if len(mel.shape) < 3: + mel = mel.unsqueeze(0) # ensure [B, C, T] + + if self.split: + frames_per_seg = math.ceil(self.segment_size / self.hop_size) + + if audio.size(1) >= self.segment_size: + mel_start = random.randint(0, mel.size(2) - frames_per_seg - 1) + mel = mel[:, :, mel_start : mel_start + frames_per_seg] + audio = audio[ + :, + mel_start + * self.hop_size : (mel_start + frames_per_seg) + * self.hop_size, + ] + + # Pad pre-computed mel and audio to match length to ensuring fine-tuning without error. + # NOTE: this may introduce a single-frame misalignment of the + # To remove possible misalignment, it is recommended to prepare the pair where the audio length is the integer multiple of self.hop_size + mel = torch.nn.functional.pad( + mel, (0, frames_per_seg - mel.size(2)), "constant" + ) + audio = torch.nn.functional.pad( + audio, (0, self.segment_size - audio.size(1)), "constant" + ) + + # Compute mel_loss used by spectral regression objective. Uses self.fmax_loss instead (usually None) + mel_loss = mel_spectrogram( + audio, + self.n_fft, + self.num_mels, + self.sampling_rate, + self.hop_size, + self.win_size, + self.fmin, + self.fmax_loss, + center=False, + ) # [B(1), self.num_mels, self.segment_size // self.hop_size] + + # Shape sanity checks + assert ( + audio.shape[1] == mel.shape[2] * self.hop_size + and audio.shape[1] == mel_loss.shape[2] * self.hop_size + ), f"Audio length must be mel frame length * hop_size. Got audio shape {audio.shape} mel shape {mel.shape} mel_loss shape {mel_loss.shape}" + + return (mel.squeeze(), audio.squeeze(0), filename, mel_loss.squeeze()) + + # If it encounters error during loading the data, skip this sample and load random other sample to the batch + except Exception as e: + if self.fine_tuning: + raise e # Terminate training if it is fine-tuning. The dataset should have been prepared properly. + else: + print( + f"[WARNING] Failed to load waveform, skipping! filename: {filename} Error: {e}" + ) + return self[random.randrange(len(self))] + + def __len__(self): + return len(self.audio_files) diff --git a/src/third_party/BigVGAN/nv-modelcard++/.gitkeep b/src/third_party/BigVGAN/nv-modelcard++/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/third_party/BigVGAN/nv-modelcard++/bias.md b/src/third_party/BigVGAN/nv-modelcard++/bias.md new file mode 100644 index 0000000000000000000000000000000000000000..68db9cd0a424393e3c2fb8bdf52c194e27ac4e8b --- /dev/null +++ b/src/third_party/BigVGAN/nv-modelcard++/bias.md @@ -0,0 +1,4 @@ +| Field | Response | +| :--------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | +| Participation considerations from adversely impacted groups protected classes in model design and testing: | None | +| Measures taken to mitigate against unwanted bias: | No measures taken to mitigate against unwanted bias. | diff --git a/src/third_party/BigVGAN/nv-modelcard++/explainability.md b/src/third_party/BigVGAN/nv-modelcard++/explainability.md new file mode 100644 index 0000000000000000000000000000000000000000..26a6ccf26376d60ee43d4717b5bbefcfea07e0d5 --- /dev/null +++ b/src/third_party/BigVGAN/nv-modelcard++/explainability.md @@ -0,0 +1,13 @@ +| Field | Response | +| :---------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Intended Application & Domain: | Generating waveform from mel spectrogram. | +| Model Type: | Convolutional Neural Network (CNN) | +| Intended Users: | This model is intended for developers to synthesize and generate waveforms from the AI-generated mel spectrograms. | +| Output: | Audio Waveform | +| Describe how the model works: | Model generates audio waveform corresponding to the input mel spectrogram. | +| Name the adversely impacted groups this has been tested to deliver comparable outcomes regardless of: | Not Applicable | +| Technical Limitations: | This may not perform well on synthetically-generated mel spectrograms that deviate significantly from the profile of mel spectrograms on which this was trained. | +| Verified to have met prescribed NVIDIA quality standards: | Yes | +| Performance Metrics: | Perceptual Evaluation of Speech Quality (PESQ), Virtual Speech Quality Objective Listener (VISQOL), Multi-resolution STFT (MRSTFT), Mel cepstral distortion (MCD), Periodicity RMSE, Voice/Unvoiced F1 Score (V/UV F1) | +| Potential Known Risks: | This model may generate low-quality or distorted soundwaves. | +| Licensing: | https://github.com/NVIDIA/BigVGAN/blob/main/LICENSE | diff --git a/src/third_party/BigVGAN/nv-modelcard++/overview.md b/src/third_party/BigVGAN/nv-modelcard++/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..681d18df3e38c4400f11e522cbb3c55b06809d8b --- /dev/null +++ b/src/third_party/BigVGAN/nv-modelcard++/overview.md @@ -0,0 +1,126 @@ +# Model Overview + +## Description: + +BigVGAN is a generative AI model specialized in synthesizing audio waveforms using Mel spectrogram as inputs. + +
+ +BigVGAN is a fully convolutional architecture with several upsampling blocks using transposed convolution followed by multiple residual dilated convolution layers. + +BigVGAN consists of a novel module, called anti-aliased multi-periodicity composition (AMP), which is specifically designed for generating waveforms. AMP is specialized in synthesizing high-frequency and periodic soundwaves drawing inspiration from audio signal processing principles. + +It applies a periodic activation function, called Snake, which provides an inductive bias to the architecture in generating periodic soundwaves. It also applies anti-aliasing filters to reduce undesired artifacts in the generated waveforms.
+ +This model is ready for commercial use.
+ +## References(s): + +- [BigVGAN: A Universal Neural Vocoder with Large-Scale Training](https://arxiv.org/abs/2206.04658)
+- [Project Page](https://research.nvidia.com/labs/adlr/projects/bigvgan/)
+- [Audio Demo](https://bigvgan-demo.github.io/)
+ +## Model Architecture: + +**Architecture Type:** Convolution Neural Network (CNN)
+**Network Architecture:** You can see the details of this model on this link: https://github.com/NVIDIA/BigVGAN and the related paper can be found here: https://arxiv.org/abs/2206.04658
+**Model Version:** 2.0
+ +## Input: + +**Input Type:** Audio
+**Input Format:** Mel Spectrogram
+**Input Parameters:** None
+**Other Properties Related to Input:** The input mel spectrogram has shape `[batch, channels, frames]`, where `channels` refers to the number of mel bands defined by the model and `frames` refers to the temporal length. The model supports arbitrary long `frames` that fits into the GPU memory. + +## Output: + +**Input Type:** Audio
+**Output Format:** Audio Waveform
+**Output Parameters:** None
+**Other Properties Related to Output:** The output audio waveform has shape `[batch, 1, time]`, where `1` refers to the mono audio channels and `time` refers to the temporal length. `time` is defined as a fixed integer multiple of input `frames`, which is an upsampling ratio of the model (`time = upsampling ratio * frames`). The output audio waveform consitutes float values with a range of `[-1, 1]`. + +## Software Integration: + +**Runtime Engine(s):** PyTorch + +**Supported Hardware Microarchitecture Compatibility:** NVIDIA Ampere, NVIDIA Hopper, NVIDIA Lovelace, NVIDIA Turing, NVIDIA Volta
+ +## Preferred/Supported Operating System(s): + +Linux + +## Model Version(s): + +v2.0 + +## Training, Testing, and Evaluation Datasets: + +### Training Dataset: + +The dataset contains diverse audio types, including speech in multiple languages, environmental sounds, and instruments. + +**Links:** + +- [AAM: Artificial Audio Multitracks Dataset](https://zenodo.org/records/5794629) +- [AudioCaps](https://audiocaps.github.io/) +- [AudioSet](https://research.google.com/audioset/index.html) +- [common-accent](https://huggingface.co/datasets/DTU54DL/common-accent) +- [Crowd Sourced Emotional Multimodal Actors Dataset (CREMA-D)](https://ieeexplore.ieee.org/document/6849440) +- [DCASE2017 Challenge, Task 4: Large-scale weakly supervised sound event detection for smart cars](https://dcase.community/challenge2017/task-large-scale-sound-event-detection) +- [FSDnoisy18k](https://zenodo.org/records/2529934) +- [Free Universal Sound Separation Dataset](https://zenodo.org/records/3694384) +- [Greatest Hits dataset](https://andrewowens.com/vis/) +- [GTZAN](https://ieeexplore.ieee.org/document/1021072) +- [JL corpus](https://www.kaggle.com/datasets/tli725/jl-corpus) +- [Medley-solos-DB: a cross-collection dataset for musical instrument recognition](https://zenodo.org/records/3464194) +- [MUSAN: A Music, Speech, and Noise Corpus](https://www.openslr.org/17/) +- [MusicBench](https://huggingface.co/datasets/amaai-lab/MusicBench) +- [MusicCaps](https://www.kaggle.com/datasets/googleai/musiccaps) +- [MusicNet](https://www.kaggle.com/datasets/imsparsh/musicnet-dataset) +- [NSynth](https://magenta.tensorflow.org/datasets/nsynth) +- [OnAir-Music-Dataset](https://github.com/sevagh/OnAir-Music-Dataset) +- [Audio Piano Triads Dataset](https://zenodo.org/records/4740877) +- [Pitch Audio Dataset (Surge synthesizer)](https://zenodo.org/records/4677097) +- [SONYC Urban Sound Tagging (SONYC-UST): a multilabel dataset from an urban acoustic sensor network](https://zenodo.org/records/3966543) +- [VocalSound: A Dataset for Improving Human Vocal Sounds Recognition](https://arxiv.org/abs/2205.03433) +- [WavText5K](https://github.com/microsoft/WavText5K) +- [CSS10: A Collection of Single Speaker Speech Datasets for 10 Languages](https://github.com/Kyubyong/css10) +- [Hi-Fi Multi-Speaker English TTS Dataset (Hi-Fi TTS)](https://www.openslr.org/109/) +- [IIIT-H Indic Speech Databases](http://festvox.org/databases/iiit_voices/) +- [Libri-Light: A Benchmark for ASR with Limited or No Supervision](https://arxiv.org/abs/1912.07875) +- [LibriTTS: A Corpus Derived from LibriSpeech for Text-to-Speech](https://www.openslr.org/60) +- [LibriTTS-R: A Restored Multi-Speaker Text-to-Speech Corpus](https://www.openslr.org/141/) +- [The SIWIS French Speech Synthesis Database](https://datashare.ed.ac.uk/handle/10283/2353) +- [Crowdsourced high-quality Colombian Spanish speech data set](https://openslr.org/72/) +- [TTS-Portuguese Corpus](https://github.com/Edresson/TTS-Portuguese-Corpus) +- [CSTR VCTK Corpus: English Multi-speaker Corpus for CSTR Voice Cloning Toolkit](https://datashare.ed.ac.uk/handle/10283/3443) + +\*\* Data Collection Method by dataset
+ +- Human
+ +\*\* Labeling Method by dataset (for those with labels)
+ +- Hybrid: Automated, Human, Unknown
+ +### Evaluating Dataset: + +Properties: The audio generation quality of BigVGAN is evaluated using `dev` splits of the [LibriTTS dataset](https://www.openslr.org/60/) and [Hi-Fi TTS dataset](https://www.openslr.org/109/). The datasets include speech in English language with equal balance of genders. + +\*\* Data Collection Method by dataset
+ +- Human
+ +\*\* Labeling Method by dataset
+ +- Automated
+ +## Inference: + +**Engine:** PyTorch
+**Test Hardware:** NVIDIA A100 GPU
+ +## Ethical Considerations: + +NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal model team to ensure this model meets requirements for the relevant industry and use case and addresses unforeseen product misuse. For more detailed information on ethical considerations for this model, please see the Model Card++ Explainability, Bias, Safety & Security, and Privacy Subcards. Please report security vulnerabilities or NVIDIA AI Concerns [here](https://www.nvidia.com/en-us/support/submit-security-vulnerability/). diff --git a/src/third_party/BigVGAN/nv-modelcard++/privacy.md b/src/third_party/BigVGAN/nv-modelcard++/privacy.md new file mode 100644 index 0000000000000000000000000000000000000000..6343648a81614a03910f3ac17f529b6e25b423f4 --- /dev/null +++ b/src/third_party/BigVGAN/nv-modelcard++/privacy.md @@ -0,0 +1,14 @@ +| Field | Response | +| :------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------- | +| Generatable or reverse engineerable personal information? | None | +| Protected class data used to create this model? | None | +| Was consent obtained for any personal data used? | Not Applicable (No Personal Data) | +| How often is dataset reviewed? | Before Release | +| Is a mechanism in place to honor data subject right of access or deletion of personal data? | Not Applicable | +| If personal collected for the development of the model, was it collected directly by NVIDIA? | Not Applicable | +| If personal collected for the development of the model by NVIDIA, do you maintain or have access to disclosures made to data subjects? | Not Applicable | +| If personal collected for the development of this AI model, was it minimized to only what was required? | Not Applicable | +| Is data in dataset traceable? | Yes | +| Is there provenance for all datasets used in training? | Yes | +| Does data labeling (annotation, metadata) comply with privacy laws? | Yes | +| Is data compliant with data subject requests for data correction or removal, if such a request was made? | No, not possible with externally-sourced data. | diff --git a/src/third_party/BigVGAN/nv-modelcard++/safety.md b/src/third_party/BigVGAN/nv-modelcard++/safety.md new file mode 100644 index 0000000000000000000000000000000000000000..42c3d57eecaa907d04beaccf5dff9d906b61e2a5 --- /dev/null +++ b/src/third_party/BigVGAN/nv-modelcard++/safety.md @@ -0,0 +1,6 @@ +| Field | Response | +| :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Model Application(s): | Synethic Audio Generation | +| Describe the life critical impact (if present). | Not Applicable | +| Use Case Restrictions: | None | +| Model and dataset restrictions: | The Principle of least privilege (PoLP) is applied limiting access for dataset generation and model development. Restrictions enforce dataset access during training, and dataset license constraints adhered to. | diff --git a/src/third_party/BigVGAN/requirements.txt b/src/third_party/BigVGAN/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ba5e7d899616f68a753cfc0f7c4ab43c3c9e9a3a --- /dev/null +++ b/src/third_party/BigVGAN/requirements.txt @@ -0,0 +1,13 @@ +torch +numpy +librosa>=0.8.1 +scipy +tensorboard +soundfile +matplotlib +pesq +auraloss +tqdm +nnAudio +ninja +huggingface_hub>=0.23.4 \ No newline at end of file diff --git a/src/third_party/BigVGAN/tests/test_activation.py b/src/third_party/BigVGAN/tests/test_activation.py new file mode 100644 index 0000000000000000000000000000000000000000..fb7d35cb605510e4a9a6227f6c6993e82e27ef6c --- /dev/null +++ b/src/third_party/BigVGAN/tests/test_activation.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import os +import sys +# to import modules from parent_dir +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) + +import torch +from alias_free_activation.cuda import activation1d +from activations import Snake + + +def test_load_fused_kernels(): + try: + print("[Success] load_fused_kernels") + except ImportError as e: + print("[Fail] load_fused_kernels") + raise e + + +def test_anti_alias_activation(): + data = torch.rand((10, 10, 200), device="cuda") + + # Check activations.Snake cuda vs. torch + fused_anti_alias_activation = activation1d.Activation1d( + activation=Snake(10), fused=True + ).cuda() + fused_activation_output = fused_anti_alias_activation(data) + + torch_anti_alias_activation = activation1d.Activation1d( + activation=Snake(10), fused=False + ).cuda() + torch_activation_output = torch_anti_alias_activation(data) + + test_result = (fused_activation_output - torch_activation_output).abs() + + while test_result.dim() != 1: + test_result = test_result.mean(dim=-1) + + diff = test_result.mean(dim=-1) + + if diff <= 1e-3: + print( + f"\n[Success] test_fused_anti_alias_activation" + f"\n > mean_difference={diff}" + f"\n > fused_values={fused_activation_output[-1][-1][:].tolist()}" + f"\n > torch_values={torch_activation_output[-1][-1][:].tolist()}" + ) + else: + print( + f"\n[Fail] test_fused_anti_alias_activation" + f"\n > mean_difference={diff}, " + f"\n > fused_values={fused_activation_output[-1][-1][:].tolist()}, " + f"\n > torch_values={torch_activation_output[-1][-1][:].tolist()}" + ) + + +if __name__ == "__main__": + from alias_free_activation.cuda import load + + load.load() + test_load_fused_kernels() + test_anti_alias_activation() diff --git a/src/third_party/BigVGAN/tests/test_activation_snake_beta.py b/src/third_party/BigVGAN/tests/test_activation_snake_beta.py new file mode 100644 index 0000000000000000000000000000000000000000..1f9aea899e2564e47c62b8b22dca4e581da64f54 --- /dev/null +++ b/src/third_party/BigVGAN/tests/test_activation_snake_beta.py @@ -0,0 +1,66 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import os +import sys +# to import modules from parent_dir +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) + +import torch +from alias_free_activation.cuda import activation1d +from activations import SnakeBeta + + +def test_load_fused_kernels(): + try: + print("[Success] load_fused_kernels") + except ImportError as e: + print("[Fail] load_fused_kernels") + raise e + + +def test_anti_alias_activation(): + data = torch.rand((10, 10, 200), device="cuda") + + # Check activations, Snake CUDA vs. Torch + fused_anti_alias_activation = activation1d.Activation1d( + activation=SnakeBeta(10), fused=True + ).cuda() + fused_activation_output = fused_anti_alias_activation(data) + + torch_anti_alias_activation = activation1d.Activation1d( + activation=SnakeBeta(10), fused=False + ).cuda() + torch_activation_output = torch_anti_alias_activation(data) + + test_result = (fused_activation_output - torch_activation_output).abs() + + while test_result.dim() != 1: + test_result = test_result.mean(dim=-1) + + diff = test_result.mean(dim=-1) + + if diff <= 1e-3: + print( + f"\n[Success] test_fused_anti_alias_activation" + f"\n > mean_difference={diff}" + f"\n > fused_values={fused_activation_output[-1][-1][:].tolist()}" + f"\n > torch_values={torch_activation_output[-1][-1][:].tolist()}" + ) + else: + print( + f"\n[Fail] test_fused_anti_alias_activation" + f"\n > mean_difference={diff}, " + f"\n > fused_values={fused_activation_output[-1][-1][:].tolist()}, " + f"\n > torch_values={torch_activation_output[-1][-1][:].tolist()}" + ) + + + +if __name__ == "__main__": + from alias_free_activation.cuda import load + + load.load() + test_load_fused_kernels() + test_anti_alias_activation() diff --git a/src/third_party/BigVGAN/tests/test_cuda_vs_torch_model.py b/src/third_party/BigVGAN/tests/test_cuda_vs_torch_model.py new file mode 100644 index 0000000000000000000000000000000000000000..fda08a19bab4170b0b3eea3e9a32815389ab9263 --- /dev/null +++ b/src/third_party/BigVGAN/tests/test_cuda_vs_torch_model.py @@ -0,0 +1,221 @@ +# Copyright (c) 2024 NVIDIA CORPORATION. +# Licensed under the MIT license. + +import os +import sys + +# to import modules from parent_dir +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) + +import torch +import json +from env import AttrDict +from bigvgan import BigVGAN +from time import time +from tqdm import tqdm +from meldataset import mel_spectrogram, MAX_WAV_VALUE +from scipy.io.wavfile import write +import numpy as np + +import argparse + +torch.backends.cudnn.benchmark = True + +# For easier debugging +torch.set_printoptions(linewidth=200, threshold=10_000) + + +def generate_soundwave(duration=5.0, sr=24000): + t = np.linspace(0, duration, int(sr * duration), False, dtype=np.float32) + + modulation = np.sin(2 * np.pi * t / duration) + + min_freq = 220 + max_freq = 1760 + frequencies = min_freq + (max_freq - min_freq) * (modulation + 1) / 2 + soundwave = np.sin(2 * np.pi * frequencies * t) + + soundwave = soundwave / np.max(np.abs(soundwave)) * 0.95 + + return soundwave, sr + + +def get_mel(x, h): + return mel_spectrogram( + x, h.n_fft, h.num_mels, h.sampling_rate, h.hop_size, h.win_size, h.fmin, h.fmax + ) + + +def load_checkpoint(filepath, device): + assert os.path.isfile(filepath) + print(f"Loading '{filepath}'") + checkpoint_dict = torch.load(filepath, map_location=device) + print("Complete.") + return checkpoint_dict + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Test script to check CUDA kernel correctness." + ) + parser.add_argument( + "--checkpoint_file", + type=str, + required=True, + help="Path to the checkpoint file. Assumes config.json exists in the directory.", + ) + + args = parser.parse_args() + + config_file = os.path.join(os.path.split(args.checkpoint_file)[0], "config.json") + with open(config_file) as f: + config = f.read() + json_config = json.loads(config) + h = AttrDict({**json_config}) + + print("loading plain Pytorch BigVGAN") + generator_original = BigVGAN(h).to("cuda") + print("loading CUDA kernel BigVGAN with auto-build") + generator_cuda_kernel = BigVGAN(h, use_cuda_kernel=True).to("cuda") + + state_dict_g = load_checkpoint(args.checkpoint_file, "cuda") + generator_original.load_state_dict(state_dict_g["generator"]) + generator_cuda_kernel.load_state_dict(state_dict_g["generator"]) + + generator_original.remove_weight_norm() + generator_original.eval() + generator_cuda_kernel.remove_weight_norm() + generator_cuda_kernel.eval() + + # define number of samples and length of mel frame to benchmark + num_sample = 10 + num_mel_frame = 16384 + + # CUDA kernel correctness check + diff = 0.0 + for i in tqdm(range(num_sample)): + # Random mel + data = torch.rand((1, h.num_mels, num_mel_frame), device="cuda") + + with torch.inference_mode(): + audio_original = generator_original(data) + + with torch.inference_mode(): + audio_cuda_kernel = generator_cuda_kernel(data) + + # Both outputs should be (almost) the same + test_result = (audio_original - audio_cuda_kernel).abs() + diff += test_result.mean(dim=-1).item() + + diff /= num_sample + if ( + diff <= 2e-3 + ): # We can expect a small difference (~1e-3) which does not affect perceptual quality + print( + f"\n[Success] test CUDA fused vs. plain torch BigVGAN inference" + f"\n > mean_difference={diff}" + f"\n > fused_values={audio_cuda_kernel[-1][-1][-30:].tolist()}" + f"\n > torch_values={audio_original[-1][-1][-30:].tolist()}" + ) + else: + print( + f"\n[Fail] test CUDA fused vs. plain torch BigVGAN inference" + f"\n > mean_difference={diff}" + f"\n > fused_values={audio_cuda_kernel[-1][-1][-30:].tolist()}, " + f"\n > torch_values={audio_original[-1][-1][-30:].tolist()}" + ) + + del data, audio_original, audio_cuda_kernel + + # Variables for tracking total time and VRAM usage + toc_total_original = 0 + toc_total_cuda_kernel = 0 + vram_used_original_total = 0 + vram_used_cuda_kernel_total = 0 + audio_length_total = 0 + + # Measure Original inference in isolation + for i in tqdm(range(num_sample)): + torch.cuda.reset_peak_memory_stats(device="cuda") + data = torch.rand((1, h.num_mels, num_mel_frame), device="cuda") + torch.cuda.synchronize() + tic = time() + with torch.inference_mode(): + audio_original = generator_original(data) + torch.cuda.synchronize() + toc = time() - tic + toc_total_original += toc + + vram_used_original_total += torch.cuda.max_memory_allocated(device="cuda") + + del data, audio_original + torch.cuda.empty_cache() + + # Measure CUDA kernel inference in isolation + for i in tqdm(range(num_sample)): + torch.cuda.reset_peak_memory_stats(device="cuda") + data = torch.rand((1, h.num_mels, num_mel_frame), device="cuda") + torch.cuda.synchronize() + tic = time() + with torch.inference_mode(): + audio_cuda_kernel = generator_cuda_kernel(data) + torch.cuda.synchronize() + toc = time() - tic + toc_total_cuda_kernel += toc + + audio_length_total += audio_cuda_kernel.shape[-1] + + vram_used_cuda_kernel_total += torch.cuda.max_memory_allocated(device="cuda") + + del data, audio_cuda_kernel + torch.cuda.empty_cache() + + # Calculate metrics + audio_second = audio_length_total / h.sampling_rate + khz_original = audio_length_total / toc_total_original / 1000 + khz_cuda_kernel = audio_length_total / toc_total_cuda_kernel / 1000 + vram_used_original_gb = vram_used_original_total / num_sample / (1024 ** 3) + vram_used_cuda_kernel_gb = vram_used_cuda_kernel_total / num_sample / (1024 ** 3) + + # Print results + print( + f"Original BigVGAN: took {toc_total_original:.2f} seconds to generate {audio_second:.2f} seconds of audio, {khz_original:.1f}kHz, {audio_second / toc_total_original:.1f} faster than realtime, VRAM used {vram_used_original_gb:.1f} GB" + ) + print( + f"CUDA kernel BigVGAN: took {toc_total_cuda_kernel:.2f} seconds to generate {audio_second:.2f} seconds of audio, {khz_cuda_kernel:.1f}kHz, {audio_second / toc_total_cuda_kernel:.1f} faster than realtime, VRAM used {vram_used_cuda_kernel_gb:.1f} GB" + ) + print(f"speedup of CUDA kernel: {khz_cuda_kernel / khz_original}") + print(f"VRAM saving of CUDA kernel: {vram_used_original_gb / vram_used_cuda_kernel_gb}") + + # Use artificial sine waves for inference test + audio_real, sr = generate_soundwave(duration=5.0, sr=h.sampling_rate) + audio_real = torch.tensor(audio_real).to("cuda") + # Compute mel spectrogram from the ground truth audio + x = get_mel(audio_real.unsqueeze(0), h) + + with torch.inference_mode(): + y_g_hat_original = generator_original(x) + y_g_hat_cuda_kernel = generator_cuda_kernel(x) + + audio_real = audio_real.squeeze() + audio_real = audio_real * MAX_WAV_VALUE + audio_real = audio_real.cpu().numpy().astype("int16") + + audio_original = y_g_hat_original.squeeze() + audio_original = audio_original * MAX_WAV_VALUE + audio_original = audio_original.cpu().numpy().astype("int16") + + audio_cuda_kernel = y_g_hat_cuda_kernel.squeeze() + audio_cuda_kernel = audio_cuda_kernel * MAX_WAV_VALUE + audio_cuda_kernel = audio_cuda_kernel.cpu().numpy().astype("int16") + + os.makedirs("tmp", exist_ok=True) + output_file_real = os.path.join("tmp", "audio_real.wav") + output_file_original = os.path.join("tmp", "audio_generated_original.wav") + output_file_cuda_kernel = os.path.join("tmp", "audio_generated_cuda_kernel.wav") + write(output_file_real, h.sampling_rate, audio_real) + write(output_file_original, h.sampling_rate, audio_original) + write(output_file_cuda_kernel, h.sampling_rate, audio_cuda_kernel) + print("Example generated audios of original vs. fused CUDA kernel written to tmp!") + print("Done") diff --git a/src/third_party/BigVGAN/train.py b/src/third_party/BigVGAN/train.py new file mode 100644 index 0000000000000000000000000000000000000000..c0745272189ed443b6c7421d21e44d6576c71f88 --- /dev/null +++ b/src/third_party/BigVGAN/train.py @@ -0,0 +1,777 @@ +# Copyright (c) 2024 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 warnings + +warnings.simplefilter(action="ignore", category=FutureWarning) +import itertools +import os +import time +import argparse +import json +import torch +import torch.nn.functional as F +from torch.utils.tensorboard import SummaryWriter +from torch.utils.data import DistributedSampler, DataLoader +import torch.multiprocessing as mp +from torch.distributed import init_process_group +from torch.nn.parallel import DistributedDataParallel +from env import AttrDict, build_env +from meldataset import MelDataset, mel_spectrogram, get_dataset_filelist, MAX_WAV_VALUE + +from bigvgan import BigVGAN +from discriminators import ( + MultiPeriodDiscriminator, + MultiResolutionDiscriminator, + MultiBandDiscriminator, + MultiScaleSubbandCQTDiscriminator, +) +from loss import ( + feature_loss, + generator_loss, + discriminator_loss, + MultiScaleMelSpectrogramLoss, +) + +from utils import ( + plot_spectrogram, + plot_spectrogram_clipped, + scan_checkpoint, + load_checkpoint, + save_checkpoint, + save_audio, +) +import torchaudio as ta +from pesq import pesq +from tqdm import tqdm +import auraloss + +torch.backends.cudnn.benchmark = False + + +def train(rank, a, h): + if h.num_gpus > 1: + # initialize distributed + init_process_group( + backend=h.dist_config["dist_backend"], + init_method=h.dist_config["dist_url"], + world_size=h.dist_config["world_size"] * h.num_gpus, + rank=rank, + ) + + # Set seed and device + torch.cuda.manual_seed(h.seed) + torch.cuda.set_device(rank) + device = torch.device(f"cuda:{rank:d}") + + # Define BigVGAN generator + generator = BigVGAN(h).to(device) + + # Define discriminators. MPD is used by default + mpd = MultiPeriodDiscriminator(h).to(device) + + # Define additional discriminators. BigVGAN-v1 uses UnivNet's MRD as default + # New in BigVGAN-v2: option to switch to new discriminators: MultiBandDiscriminator / MultiScaleSubbandCQTDiscriminator + if h.get("use_mbd_instead_of_mrd", False): # Switch to MBD + print( + "[INFO] using MultiBandDiscriminator of BigVGAN-v2 instead of MultiResolutionDiscriminator" + ) + # Variable name is kept as "mrd" for backward compatibility & minimal code change + mrd = MultiBandDiscriminator(h).to(device) + elif h.get("use_cqtd_instead_of_mrd", False): # Switch to CQTD + print( + "[INFO] using MultiScaleSubbandCQTDiscriminator of BigVGAN-v2 instead of MultiResolutionDiscriminator" + ) + mrd = MultiScaleSubbandCQTDiscriminator(h).to(device) + else: # Fallback to original MRD in BigVGAN-v1 + mrd = MultiResolutionDiscriminator(h).to(device) + + # New in BigVGAN-v2: option to switch to multi-scale L1 mel loss + if h.get("use_multiscale_melloss", False): + print( + "[INFO] using multi-scale Mel l1 loss of BigVGAN-v2 instead of the original single-scale loss" + ) + fn_mel_loss_multiscale = MultiScaleMelSpectrogramLoss( + sampling_rate=h.sampling_rate + ) # NOTE: accepts waveform as input + else: + fn_mel_loss_singlescale = F.l1_loss + + # Print the model & number of parameters, and create or scan the latest checkpoint from checkpoints directory + if rank == 0: + print(generator) + print(mpd) + print(mrd) + print(f"Generator params: {sum(p.numel() for p in generator.parameters())}") + print(f"Discriminator mpd params: {sum(p.numel() for p in mpd.parameters())}") + print(f"Discriminator mrd params: {sum(p.numel() for p in mrd.parameters())}") + os.makedirs(a.checkpoint_path, exist_ok=True) + print(f"Checkpoints directory: {a.checkpoint_path}") + + if os.path.isdir(a.checkpoint_path): + # New in v2.1: If the step prefix pattern-based checkpoints are not found, also check for renamed files in Hugging Face Hub to resume training + cp_g = scan_checkpoint( + a.checkpoint_path, prefix="g_", renamed_file="bigvgan_generator.pt" + ) + cp_do = scan_checkpoint( + a.checkpoint_path, + prefix="do_", + renamed_file="bigvgan_discriminator_optimizer.pt", + ) + + # Load the latest checkpoint if exists + steps = 0 + if cp_g is None or cp_do is None: + state_dict_do = None + last_epoch = -1 + else: + state_dict_g = load_checkpoint(cp_g, device) + state_dict_do = load_checkpoint(cp_do, device) + generator.load_state_dict(state_dict_g["generator"]) + mpd.load_state_dict(state_dict_do["mpd"]) + mrd.load_state_dict(state_dict_do["mrd"]) + steps = state_dict_do["steps"] + 1 + last_epoch = state_dict_do["epoch"] + + # Initialize DDP, optimizers, and schedulers + if h.num_gpus > 1: + generator = DistributedDataParallel(generator, device_ids=[rank]).to(device) + mpd = DistributedDataParallel(mpd, device_ids=[rank]).to(device) + mrd = DistributedDataParallel(mrd, device_ids=[rank]).to(device) + + optim_g = torch.optim.AdamW( + generator.parameters(), h.learning_rate, betas=[h.adam_b1, h.adam_b2] + ) + optim_d = torch.optim.AdamW( + itertools.chain(mrd.parameters(), mpd.parameters()), + h.learning_rate, + betas=[h.adam_b1, h.adam_b2], + ) + + if state_dict_do is not None: + optim_g.load_state_dict(state_dict_do["optim_g"]) + optim_d.load_state_dict(state_dict_do["optim_d"]) + + scheduler_g = torch.optim.lr_scheduler.ExponentialLR( + optim_g, gamma=h.lr_decay, last_epoch=last_epoch + ) + scheduler_d = torch.optim.lr_scheduler.ExponentialLR( + optim_d, gamma=h.lr_decay, last_epoch=last_epoch + ) + + # Define training and validation datasets + + """ + unseen_validation_filelist will contain sample filepaths outside the seen training & validation dataset + Example: trained on LibriTTS, validate on VCTK + """ + training_filelist, validation_filelist, list_unseen_validation_filelist = ( + get_dataset_filelist(a) + ) + + trainset = MelDataset( + training_filelist, + h, + h.segment_size, + h.n_fft, + h.num_mels, + h.hop_size, + h.win_size, + h.sampling_rate, + h.fmin, + h.fmax, + shuffle=False if h.num_gpus > 1 else True, + fmax_loss=h.fmax_for_loss, + device=device, + fine_tuning=a.fine_tuning, + base_mels_path=a.input_mels_dir, + is_seen=True, + ) + + train_sampler = DistributedSampler(trainset) if h.num_gpus > 1 else None + + train_loader = DataLoader( + trainset, + num_workers=h.num_workers, + shuffle=False, + sampler=train_sampler, + batch_size=h.batch_size, + pin_memory=True, + drop_last=True, + ) + + if rank == 0: + validset = MelDataset( + validation_filelist, + h, + h.segment_size, + h.n_fft, + h.num_mels, + h.hop_size, + h.win_size, + h.sampling_rate, + h.fmin, + h.fmax, + False, + False, + fmax_loss=h.fmax_for_loss, + device=device, + fine_tuning=a.fine_tuning, + base_mels_path=a.input_mels_dir, + is_seen=True, + ) + validation_loader = DataLoader( + validset, + num_workers=1, + shuffle=False, + sampler=None, + batch_size=1, + pin_memory=True, + drop_last=True, + ) + + list_unseen_validset = [] + list_unseen_validation_loader = [] + for i in range(len(list_unseen_validation_filelist)): + unseen_validset = MelDataset( + list_unseen_validation_filelist[i], + h, + h.segment_size, + h.n_fft, + h.num_mels, + h.hop_size, + h.win_size, + h.sampling_rate, + h.fmin, + h.fmax, + False, + False, + fmax_loss=h.fmax_for_loss, + device=device, + fine_tuning=a.fine_tuning, + base_mels_path=a.input_mels_dir, + is_seen=False, + ) + unseen_validation_loader = DataLoader( + unseen_validset, + num_workers=1, + shuffle=False, + sampler=None, + batch_size=1, + pin_memory=True, + drop_last=True, + ) + list_unseen_validset.append(unseen_validset) + list_unseen_validation_loader.append(unseen_validation_loader) + + # Tensorboard logger + sw = SummaryWriter(os.path.join(a.checkpoint_path, "logs")) + if a.save_audio: # Also save audio to disk if --save_audio is set to True + os.makedirs(os.path.join(a.checkpoint_path, "samples"), exist_ok=True) + + """ + Validation loop, "mode" parameter is automatically defined as (seen or unseen)_(name of the dataset). + If the name of the dataset contains "nonspeech", it skips PESQ calculation to prevent errors + """ + + def validate(rank, a, h, loader, mode="seen"): + assert rank == 0, "validate should only run on rank=0" + generator.eval() + torch.cuda.empty_cache() + + val_err_tot = 0 + val_pesq_tot = 0 + val_mrstft_tot = 0 + + # Modules for evaluation metrics + pesq_resampler = ta.transforms.Resample(h.sampling_rate, 16000).cuda() + loss_mrstft = auraloss.freq.MultiResolutionSTFTLoss(device="cuda") + + if a.save_audio: # Also save audio to disk if --save_audio is set to True + os.makedirs( + os.path.join(a.checkpoint_path, "samples", f"gt_{mode}"), + exist_ok=True, + ) + os.makedirs( + os.path.join(a.checkpoint_path, "samples", f"{mode}_{steps:08d}"), + exist_ok=True, + ) + + with torch.no_grad(): + print(f"step {steps} {mode} speaker validation...") + + # Loop over validation set and compute metrics + for j, batch in enumerate(tqdm(loader)): + x, y, _, y_mel = batch + y = y.to(device) + if hasattr(generator, "module"): + y_g_hat = generator.module(x.to(device)) + else: + y_g_hat = generator(x.to(device)) + y_mel = y_mel.to(device, non_blocking=True) + y_g_hat_mel = mel_spectrogram( + y_g_hat.squeeze(1), + h.n_fft, + h.num_mels, + h.sampling_rate, + h.hop_size, + h.win_size, + h.fmin, + h.fmax_for_loss, + ) + min_t = min(y_mel.size(-1), y_g_hat_mel.size(-1)) + val_err_tot += F.l1_loss(y_mel[...,:min_t], y_g_hat_mel[...,:min_t]).item() + + # PESQ calculation. only evaluate PESQ if it's speech signal (nonspeech PESQ will error out) + if ( + not "nonspeech" in mode + ): # Skips if the name of dataset (in mode string) contains "nonspeech" + + # Resample to 16000 for pesq + y_16k = pesq_resampler(y) + y_g_hat_16k = pesq_resampler(y_g_hat.squeeze(1)) + y_int_16k = (y_16k[0] * MAX_WAV_VALUE).short().cpu().numpy() + y_g_hat_int_16k = ( + (y_g_hat_16k[0] * MAX_WAV_VALUE).short().cpu().numpy() + ) + val_pesq_tot += pesq(16000, y_int_16k, y_g_hat_int_16k, "wb") + + # MRSTFT calculation + min_t = min(y.size(-1), y_g_hat.size(-1)) + val_mrstft_tot += loss_mrstft(y_g_hat[...,:min_t], y[...,:min_t]).item() + + # Log audio and figures to Tensorboard + if j % a.eval_subsample == 0: # Subsample every nth from validation set + if steps >= 0: + sw.add_audio(f"gt_{mode}/y_{j}", y[0], steps, h.sampling_rate) + if ( + a.save_audio + ): # Also save audio to disk if --save_audio is set to True + save_audio( + y[0], + os.path.join( + a.checkpoint_path, + "samples", + f"gt_{mode}", + f"{j:04d}.wav", + ), + h.sampling_rate, + ) + sw.add_figure( + f"gt_{mode}/y_spec_{j}", + plot_spectrogram(x[0]), + steps, + ) + + sw.add_audio( + f"generated_{mode}/y_hat_{j}", + y_g_hat[0], + steps, + h.sampling_rate, + ) + if ( + a.save_audio + ): # Also save audio to disk if --save_audio is set to True + save_audio( + y_g_hat[0, 0], + os.path.join( + a.checkpoint_path, + "samples", + f"{mode}_{steps:08d}", + f"{j:04d}.wav", + ), + h.sampling_rate, + ) + # Spectrogram of synthesized audio + y_hat_spec = mel_spectrogram( + y_g_hat.squeeze(1), + h.n_fft, + h.num_mels, + h.sampling_rate, + h.hop_size, + h.win_size, + h.fmin, + h.fmax, + ) + sw.add_figure( + f"generated_{mode}/y_hat_spec_{j}", + plot_spectrogram(y_hat_spec.squeeze(0).cpu().numpy()), + steps, + ) + + """ + Visualization of spectrogram difference between GT and synthesized audio, difference higher than 1 is clipped for better visualization. + """ + spec_delta = torch.clamp( + torch.abs(x[0] - y_hat_spec.squeeze(0).cpu()), + min=1e-6, + max=1.0, + ) + sw.add_figure( + f"delta_dclip1_{mode}/spec_{j}", + plot_spectrogram_clipped(spec_delta.numpy(), clip_max=1.0), + steps, + ) + + val_err = val_err_tot / (j + 1) + val_pesq = val_pesq_tot / (j + 1) + val_mrstft = val_mrstft_tot / (j + 1) + # Log evaluation metrics to Tensorboard + sw.add_scalar(f"validation_{mode}/mel_spec_error", val_err, steps) + sw.add_scalar(f"validation_{mode}/pesq", val_pesq, steps) + sw.add_scalar(f"validation_{mode}/mrstft", val_mrstft, steps) + + generator.train() + + # If the checkpoint is loaded, start with validation loop + if steps != 0 and rank == 0 and not a.debug: + if not a.skip_seen: + validate( + rank, + a, + h, + validation_loader, + mode=f"seen_{train_loader.dataset.name}", + ) + for i in range(len(list_unseen_validation_loader)): + validate( + rank, + a, + h, + list_unseen_validation_loader[i], + mode=f"unseen_{list_unseen_validation_loader[i].dataset.name}", + ) + # Exit the script if --evaluate is set to True + if a.evaluate: + exit() + + # Main training loop + generator.train() + mpd.train() + mrd.train() + for epoch in range(max(0, last_epoch), a.training_epochs): + if rank == 0: + start = time.time() + print(f"Epoch: {epoch + 1}") + + if h.num_gpus > 1: + train_sampler.set_epoch(epoch) + + for i, batch in enumerate(train_loader): + if rank == 0: + start_b = time.time() + x, y, _, y_mel = batch + + x = x.to(device, non_blocking=True) + y = y.to(device, non_blocking=True) + y_mel = y_mel.to(device, non_blocking=True) + y = y.unsqueeze(1) + + y_g_hat = generator(x) + y_g_hat_mel = mel_spectrogram( + y_g_hat.squeeze(1), + h.n_fft, + h.num_mels, + h.sampling_rate, + h.hop_size, + h.win_size, + h.fmin, + h.fmax_for_loss, + ) + + optim_d.zero_grad() + + # MPD + y_df_hat_r, y_df_hat_g, _, _ = mpd(y, y_g_hat.detach()) + loss_disc_f, losses_disc_f_r, losses_disc_f_g = discriminator_loss( + y_df_hat_r, y_df_hat_g + ) + + # MRD + y_ds_hat_r, y_ds_hat_g, _, _ = mrd(y, y_g_hat.detach()) + loss_disc_s, losses_disc_s_r, losses_disc_s_g = discriminator_loss( + y_ds_hat_r, y_ds_hat_g + ) + + loss_disc_all = loss_disc_s + loss_disc_f + + # Set clip_grad_norm value + clip_grad_norm = h.get("clip_grad_norm", 1000.0) # Default to 1000 + + # Whether to freeze D for initial training steps + if steps >= a.freeze_step: + loss_disc_all.backward() + grad_norm_mpd = torch.nn.utils.clip_grad_norm_( + mpd.parameters(), clip_grad_norm + ) + grad_norm_mrd = torch.nn.utils.clip_grad_norm_( + mrd.parameters(), clip_grad_norm + ) + optim_d.step() + else: + print( + f"[WARNING] skipping D training for the first {a.freeze_step} steps" + ) + grad_norm_mpd = 0.0 + grad_norm_mrd = 0.0 + + # Generator + optim_g.zero_grad() + + # L1 Mel-Spectrogram Loss + lambda_melloss = h.get( + "lambda_melloss", 45.0 + ) # Defaults to 45 in BigVGAN-v1 if not set + if h.get("use_multiscale_melloss", False): # uses wav for loss + loss_mel = fn_mel_loss_multiscale(y, y_g_hat) * lambda_melloss + else: # Uses mel for loss + loss_mel = fn_mel_loss_singlescale(y_mel, y_g_hat_mel) * lambda_melloss + + # MPD loss + y_df_hat_r, y_df_hat_g, fmap_f_r, fmap_f_g = mpd(y, y_g_hat) + loss_fm_f = feature_loss(fmap_f_r, fmap_f_g) + loss_gen_f, losses_gen_f = generator_loss(y_df_hat_g) + + # MRD loss + y_ds_hat_r, y_ds_hat_g, fmap_s_r, fmap_s_g = mrd(y, y_g_hat) + loss_fm_s = feature_loss(fmap_s_r, fmap_s_g) + loss_gen_s, losses_gen_s = generator_loss(y_ds_hat_g) + + if steps >= a.freeze_step: + loss_gen_all = ( + loss_gen_s + loss_gen_f + loss_fm_s + loss_fm_f + loss_mel + ) + else: + print( + f"[WARNING] using regression loss only for G for the first {a.freeze_step} steps" + ) + loss_gen_all = loss_mel + + loss_gen_all.backward() + grad_norm_g = torch.nn.utils.clip_grad_norm_( + generator.parameters(), clip_grad_norm + ) + optim_g.step() + + if rank == 0: + # STDOUT logging + if steps % a.stdout_interval == 0: + mel_error = ( + loss_mel.item() / lambda_melloss + ) # Log training mel regression loss to stdout + print( + f"Steps: {steps:d}, " + f"Gen Loss Total: {loss_gen_all:4.3f}, " + f"Mel Error: {mel_error:4.3f}, " + f"s/b: {time.time() - start_b:4.3f} " + f"lr: {optim_g.param_groups[0]['lr']:4.7f} " + f"grad_norm_g: {grad_norm_g:4.3f}" + ) + + # Checkpointing + if steps % a.checkpoint_interval == 0 and steps != 0: + checkpoint_path = f"{a.checkpoint_path}/g_{steps:08d}" + save_checkpoint( + checkpoint_path, + { + "generator": ( + generator.module if h.num_gpus > 1 else generator + ).state_dict() + }, + ) + checkpoint_path = f"{a.checkpoint_path}/do_{steps:08d}" + save_checkpoint( + checkpoint_path, + { + "mpd": (mpd.module if h.num_gpus > 1 else mpd).state_dict(), + "mrd": (mrd.module if h.num_gpus > 1 else mrd).state_dict(), + "optim_g": optim_g.state_dict(), + "optim_d": optim_d.state_dict(), + "steps": steps, + "epoch": epoch, + }, + ) + + # Tensorboard summary logging + if steps % a.summary_interval == 0: + mel_error = ( + loss_mel.item() / lambda_melloss + ) # Log training mel regression loss to tensorboard + sw.add_scalar("training/gen_loss_total", loss_gen_all.item(), steps) + sw.add_scalar("training/mel_spec_error", mel_error, steps) + sw.add_scalar("training/fm_loss_mpd", loss_fm_f.item(), steps) + sw.add_scalar("training/gen_loss_mpd", loss_gen_f.item(), steps) + sw.add_scalar("training/disc_loss_mpd", loss_disc_f.item(), steps) + sw.add_scalar("training/grad_norm_mpd", grad_norm_mpd, steps) + sw.add_scalar("training/fm_loss_mrd", loss_fm_s.item(), steps) + sw.add_scalar("training/gen_loss_mrd", loss_gen_s.item(), steps) + sw.add_scalar("training/disc_loss_mrd", loss_disc_s.item(), steps) + sw.add_scalar("training/grad_norm_mrd", grad_norm_mrd, steps) + sw.add_scalar("training/grad_norm_g", grad_norm_g, steps) + sw.add_scalar( + "training/learning_rate_d", scheduler_d.get_last_lr()[0], steps + ) + sw.add_scalar( + "training/learning_rate_g", scheduler_g.get_last_lr()[0], steps + ) + sw.add_scalar("training/epoch", epoch + 1, steps) + + # Validation + if steps % a.validation_interval == 0: + # Plot training input x so far used + for i_x in range(x.shape[0]): + sw.add_figure( + f"training_input/x_{i_x}", + plot_spectrogram(x[i_x].cpu()), + steps, + ) + sw.add_audio( + f"training_input/y_{i_x}", + y[i_x][0], + steps, + h.sampling_rate, + ) + + # Seen and unseen speakers validation loops + if not a.debug and steps != 0: + validate( + rank, + a, + h, + validation_loader, + mode=f"seen_{train_loader.dataset.name}", + ) + for i in range(len(list_unseen_validation_loader)): + validate( + rank, + a, + h, + list_unseen_validation_loader[i], + mode=f"unseen_{list_unseen_validation_loader[i].dataset.name}", + ) + steps += 1 + + # BigVGAN-v2 learning rate scheduler is changed from epoch-level to step-level + scheduler_g.step() + scheduler_d.step() + + if rank == 0: + print( + f"Time taken for epoch {epoch + 1} is {int(time.time() - start)} sec\n" + ) + + +def main(): + print("Initializing Training Process..") + + parser = argparse.ArgumentParser() + + parser.add_argument("--group_name", default=None) + + parser.add_argument("--input_wavs_dir", default="LibriTTS") + parser.add_argument("--input_mels_dir", default="ft_dataset") + parser.add_argument( + "--input_training_file", default="tests/LibriTTS/train-full.txt" + ) + parser.add_argument( + "--input_validation_file", default="tests/LibriTTS/val-full.txt" + ) + + parser.add_argument( + "--list_input_unseen_wavs_dir", + nargs="+", + default=["tests/LibriTTS", "tests/LibriTTS"], + ) + parser.add_argument( + "--list_input_unseen_validation_file", + nargs="+", + default=["tests/LibriTTS/dev-clean.txt", "tests/LibriTTS/dev-other.txt"], + ) + + parser.add_argument("--checkpoint_path", default="exp/bigvgan") + parser.add_argument("--config", default="") + + parser.add_argument("--training_epochs", default=100000, type=int) + parser.add_argument("--stdout_interval", default=5, type=int) + parser.add_argument("--checkpoint_interval", default=50000, type=int) + parser.add_argument("--summary_interval", default=100, type=int) + parser.add_argument("--validation_interval", default=50000, type=int) + + parser.add_argument( + "--freeze_step", + default=0, + type=int, + help="freeze D for the first specified steps. G only uses regression loss for these steps.", + ) + + parser.add_argument("--fine_tuning", default=False, type=bool) + + parser.add_argument( + "--debug", + default=False, + type=bool, + help="debug mode. skips validation loop throughout training", + ) + parser.add_argument( + "--evaluate", + default=False, + type=bool, + help="only run evaluation from checkpoint and exit", + ) + parser.add_argument( + "--eval_subsample", + default=5, + type=int, + help="subsampling during evaluation loop", + ) + parser.add_argument( + "--skip_seen", + default=False, + type=bool, + help="skip seen dataset. useful for test set inference", + ) + parser.add_argument( + "--save_audio", + default=False, + type=bool, + help="save audio of test set inference to disk", + ) + + a = parser.parse_args() + + with open(a.config) as f: + data = f.read() + + json_config = json.loads(data) + h = AttrDict(json_config) + + build_env(a.config, "config.json", a.checkpoint_path) + + torch.manual_seed(h.seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(h.seed) + h.num_gpus = torch.cuda.device_count() + h.batch_size = int(h.batch_size / h.num_gpus) + print(f"Batch size per GPU: {h.batch_size}") + else: + pass + + if h.num_gpus > 1: + mp.spawn( + train, + nprocs=h.num_gpus, + args=( + a, + h, + ), + ) + else: + train(0, a, h) + + +if __name__ == "__main__": + main() diff --git a/src/third_party/BigVGAN/utils.py b/src/third_party/BigVGAN/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fc64ae591d7ec40e69146e626a7570414495d243 --- /dev/null +++ b/src/third_party/BigVGAN/utils.py @@ -0,0 +1,99 @@ +# Adapted from https://github.com/jik876/hifi-gan under the MIT license. +# LICENSE is in incl_licenses directory. + +import glob +import os +import matplotlib +import torch +from torch.nn.utils import weight_norm + +matplotlib.use("Agg") +import matplotlib.pylab as plt +from meldataset import MAX_WAV_VALUE +from scipy.io.wavfile import write + + +def plot_spectrogram(spectrogram): + fig, ax = plt.subplots(figsize=(10, 2)) + im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none") + plt.colorbar(im, ax=ax) + + fig.canvas.draw() + plt.close() + + return fig + + +def plot_spectrogram_clipped(spectrogram, clip_max=2.0): + fig, ax = plt.subplots(figsize=(10, 2)) + im = ax.imshow( + spectrogram, + aspect="auto", + origin="lower", + interpolation="none", + vmin=1e-6, + vmax=clip_max, + ) + plt.colorbar(im, ax=ax) + + fig.canvas.draw() + plt.close() + + return fig + + +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 apply_weight_norm(m): + classname = m.__class__.__name__ + if classname.find("Conv") != -1: + weight_norm(m) + + +def get_padding(kernel_size, dilation=1): + return int((kernel_size * dilation - dilation) / 2) + + +def load_checkpoint(filepath, device): + assert os.path.isfile(filepath) + print(f"Loading '{filepath}'") + checkpoint_dict = torch.load(filepath, map_location=device) + print("Complete.") + return checkpoint_dict + + +def save_checkpoint(filepath, obj): + print(f"Saving checkpoint to {filepath}") + torch.save(obj, filepath) + print("Complete.") + + +def scan_checkpoint(cp_dir, prefix, renamed_file=None): + # Fallback to original scanning logic first + pattern = os.path.join(cp_dir, prefix + "????????") + cp_list = glob.glob(pattern) + + if len(cp_list) > 0: + last_checkpoint_path = sorted(cp_list)[-1] + print(f"[INFO] Resuming from checkpoint: '{last_checkpoint_path}'") + return last_checkpoint_path + + # If no pattern-based checkpoints are found, check for renamed file + if renamed_file: + renamed_path = os.path.join(cp_dir, renamed_file) + if os.path.isfile(renamed_path): + print(f"[INFO] Resuming from renamed checkpoint: '{renamed_file}'") + return renamed_path + + return None + + +def save_audio(audio, path, sr): + # wav: torch with 1d shape + audio = audio * MAX_WAV_VALUE + audio = audio.cpu().numpy().astype("int16") + write(path, sr, audio)