soiz1 commited on
Commit
9aaf513
·
verified ·
1 Parent(s): f201737

Upload 109 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +10 -0
  2. .github/FUNDING.yml +13 -0
  3. .github/ISSUE_TEMPLATE/bug_report.md +11 -0
  4. .github/ISSUE_TEMPLATE/feature_request.md +10 -0
  5. .github/ISSUE_TEMPLATE/hallucination.md +12 -0
  6. .github/pull_request_template.md +5 -0
  7. .github/workflows/ci.yml +101 -0
  8. .github/workflows/publish-docker.yml +73 -0
  9. .gitignore +13 -0
  10. Dockerfile +34 -0
  11. Install.bat +21 -0
  12. Install.sh +18 -0
  13. LICENSE +201 -0
  14. README.md +134 -12
  15. app.py +368 -0
  16. backend/Dockerfile +36 -0
  17. backend/README.md +110 -0
  18. backend/__init__.py +0 -0
  19. backend/cache/cached_files_are_generated_here +0 -0
  20. backend/common/audio.py +36 -0
  21. backend/common/cache_manager.py +21 -0
  22. backend/common/compresser.py +58 -0
  23. backend/common/config_loader.py +25 -0
  24. backend/common/models.py +14 -0
  25. backend/configs/config.yaml +23 -0
  26. backend/db/__init__.py +0 -0
  27. backend/db/db_instance.py +42 -0
  28. backend/db/task/__init__.py +0 -0
  29. backend/db/task/dao.py +94 -0
  30. backend/db/task/models.py +174 -0
  31. backend/docker-compose.yaml +33 -0
  32. backend/main.py +92 -0
  33. backend/nginx/logs/logs_are_generated_here +0 -0
  34. backend/nginx/nginx.conf +23 -0
  35. backend/nginx/temp/temps_are_generated_here +0 -0
  36. backend/requirements-backend.txt +13 -0
  37. backend/routers/__init__.py +0 -0
  38. backend/routers/bgm_separation/__init__.py +0 -0
  39. backend/routers/bgm_separation/models.py +6 -0
  40. backend/routers/bgm_separation/router.py +119 -0
  41. backend/routers/task/__init__.py +0 -0
  42. backend/routers/task/router.py +130 -0
  43. backend/routers/transcription/__init__.py +0 -0
  44. backend/routers/transcription/router.py +123 -0
  45. backend/routers/vad/__init__.py +0 -0
  46. backend/routers/vad/router.py +101 -0
  47. backend/tests/__init__.py +0 -0
  48. backend/tests/test_backend_bgm_separation.py +59 -0
  49. backend/tests/test_backend_config.py +67 -0
  50. backend/tests/test_backend_transcription.py +50 -0
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # from .gitignore
2
+ modules/yt_tmp.wav
3
+ **/venv/
4
+ **/__pycache__/
5
+ **/outputs/
6
+ **/models/
7
+
8
+ **/.idea
9
+ **/.git
10
+ **/.github
.github/FUNDING.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # These are supported funding model platforms
2
+
3
+ github: []
4
+ patreon: # Replace with a single Patreon username
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: jhj0517
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ otechie: # Replace with a single Otechie username
12
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
.github/ISSUE_TEMPLATE/bug_report.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: bug
6
+ assignees: jhj0517
7
+
8
+ ---
9
+
10
+ **Which OS are you using?**
11
+ - OS: [e.g. iOS or Windows.. If you are using Google Colab, just Colab.]
.github/ISSUE_TEMPLATE/feature_request.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Feature request
3
+ about: Any feature you want
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: jhj0517
7
+
8
+ ---
9
+
10
+
.github/ISSUE_TEMPLATE/hallucination.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Hallucination
3
+ about: Whisper hallucinations. ( Repeating certain words or subtitles starting too
4
+ early, etc. )
5
+ title: ''
6
+ labels: hallucination
7
+ assignees: jhj0517
8
+
9
+ ---
10
+
11
+ **Download URL for sample audio**
12
+ - Please upload download URL for sample audio file so I can test with some settings for better result. You can use https://easyupload.io/ or any other service to share.
.github/pull_request_template.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ## Related issues / PRs. Summarize issues.
2
+ - #
3
+
4
+ ## Summarize Changes
5
+ 1.
.github/workflows/ci.yml ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ push:
7
+ branches:
8
+ - master
9
+ - intel-gpu
10
+ pull_request:
11
+ branches:
12
+ - master
13
+ - intel-gpu
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ matrix:
20
+ python: ["3.10", "3.11", "3.12"]
21
+
22
+ env:
23
+ DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }}
24
+
25
+ steps:
26
+ - name: Clean up space for action
27
+ run: rm -rf /opt/hostedtoolcache
28
+
29
+ - uses: actions/checkout@v4
30
+ - name: Setup Python
31
+ uses: actions/setup-python@v5
32
+ with:
33
+ python-version: ${{ matrix.python }}
34
+
35
+ - name: Install git and ffmpeg
36
+ run: sudo apt-get update && sudo apt-get install -y git ffmpeg
37
+
38
+ - name: Install dependencies
39
+ run: pip install -r requirements.txt pytest jiwer
40
+
41
+ - name: Run test
42
+ run: python -m pytest -rs tests
43
+
44
+ test-backend:
45
+ runs-on: ubuntu-latest
46
+ strategy:
47
+ matrix:
48
+ python: ["3.10", "3.11", "3.12"]
49
+
50
+ env:
51
+ DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }}
52
+ TEST_ENV: true
53
+
54
+ steps:
55
+ - name: Clean up space for action
56
+ run: rm -rf /opt/hostedtoolcache
57
+
58
+ - uses: actions/checkout@v4
59
+ - name: Setup Python
60
+ uses: actions/setup-python@v5
61
+ with:
62
+ python-version: ${{ matrix.python }}
63
+
64
+ - name: Install git and ffmpeg
65
+ run: sudo apt-get update && sudo apt-get install -y git ffmpeg
66
+
67
+ - name: Install dependencies
68
+ run: pip install -r backend/requirements-backend.txt pytest pytest-asyncio jiwer
69
+
70
+ - name: Run test
71
+ run: python -m pytest -rs backend/tests
72
+
73
+ test-shell-script:
74
+ runs-on: ubuntu-latest
75
+ strategy:
76
+ matrix:
77
+ python: [ "3.10", "3.11", "3.12" ]
78
+
79
+ steps:
80
+ - name: Clean up space for action
81
+ run: rm -rf /opt/hostedtoolcache
82
+
83
+ - uses: actions/checkout@v4
84
+ - name: Setup Python
85
+ uses: actions/setup-python@v5
86
+ with:
87
+ python-version: ${{ matrix.python }}
88
+
89
+ - name: Install git and ffmpeg
90
+ run: sudo apt-get update && sudo apt-get install -y git ffmpeg
91
+
92
+ - name: Execute Install.sh
93
+ run: |
94
+ chmod +x ./Install.sh
95
+ ./Install.sh
96
+
97
+ - name: Execute start-webui.sh
98
+ run: |
99
+ chmod +x ./start-webui.sh
100
+ timeout 60s ./start-webui.sh || true
101
+
.github/workflows/publish-docker.yml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish to Docker Hub
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ build-and-push-webui:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Clean up space for action
14
+ run: rm -rf /opt/hostedtoolcache
15
+
16
+ - name: Log in to Docker Hub
17
+ uses: docker/login-action@v2
18
+ with:
19
+ username: ${{ secrets.DOCKER_USERNAME }}
20
+ password: ${{ secrets.DOCKER_PASSWORD }}
21
+
22
+ - name: Checkout repository
23
+ uses: actions/checkout@v3
24
+
25
+ - name: Set up Docker Buildx
26
+ uses: docker/setup-buildx-action@v3
27
+
28
+ - name: Set up QEMU
29
+ uses: docker/setup-qemu-action@v3
30
+
31
+ - name: Build and push Docker image
32
+ uses: docker/build-push-action@v5
33
+ with:
34
+ context: .
35
+ file: ./Dockerfile
36
+ push: true
37
+ tags: ${{ secrets.DOCKER_USERNAME }}/whisper-webui:latest
38
+
39
+ - name: Log out of Docker Hub
40
+ run: docker logout
41
+
42
+ build-and-push-backend:
43
+ runs-on: ubuntu-latest
44
+
45
+ steps:
46
+ - name: Clean up space for action
47
+ run: rm -rf /opt/hostedtoolcache
48
+
49
+ - name: Log in to Docker Hub
50
+ uses: docker/login-action@v2
51
+ with:
52
+ username: ${{ secrets.DOCKER_USERNAME }}
53
+ password: ${{ secrets.DOCKER_PASSWORD }}
54
+
55
+ - name: Checkout repository
56
+ uses: actions/checkout@v3
57
+
58
+ - name: Set up Docker Buildx
59
+ uses: docker/setup-buildx-action@v3
60
+
61
+ - name: Set up QEMU
62
+ uses: docker/setup-qemu-action@v3
63
+
64
+ - name: Build and push Docker image
65
+ uses: docker/build-push-action@v5
66
+ with:
67
+ context: .
68
+ file: ./backend/Dockerfile
69
+ push: true
70
+ tags: ${{ secrets.DOCKER_USERNAME }}/whisper-webui-backend:latest
71
+
72
+ - name: Log out of Docker Hub
73
+ run: docker logout
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.wav
2
+ *.png
3
+ *.mp4
4
+ *.mp3
5
+ **/.env
6
+ **/.idea/
7
+ **/.pytest_cache/
8
+ **/venv/
9
+ **/__pycache__/
10
+ outputs/
11
+ models/
12
+ modules/yt_tmp.wav
13
+ configs/default_parameters.yaml
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM debian:bookworm-slim AS builder
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y curl git python3 python3-pip python3-venv && \
5
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* && \
6
+ mkdir -p /Whisper-WebUI
7
+
8
+ WORKDIR /Whisper-WebUI
9
+
10
+ COPY requirements.txt .
11
+
12
+ RUN python3 -m venv venv && \
13
+ . venv/bin/activate && \
14
+ pip install -U -r requirements.txt
15
+
16
+
17
+ FROM debian:bookworm-slim AS runtime
18
+
19
+ RUN apt-get update && \
20
+ apt-get install -y curl ffmpeg python3 && \
21
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
22
+
23
+ WORKDIR /Whisper-WebUI
24
+
25
+ COPY . .
26
+ COPY --from=builder /Whisper-WebUI/venv /Whisper-WebUI/venv
27
+
28
+ VOLUME [ "/Whisper-WebUI/models" ]
29
+ VOLUME [ "/Whisper-WebUI/outputs" ]
30
+
31
+ ENV PATH="/Whisper-WebUI/venv/bin:$PATH"
32
+ ENV LD_LIBRARY_PATH=/Whisper-WebUI/venv/lib64/python3.11/site-packages/nvidia/cublas/lib:/Whisper-WebUI/venv/lib64/python3.11/site-packages/nvidia/cudnn/lib
33
+
34
+ ENTRYPOINT [ "python", "app.py" ]
Install.bat ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ if not exist "%~dp0\venv\Scripts" (
4
+ echo Creating venv...
5
+ python -m venv venv
6
+ )
7
+ echo checked the venv folder. now installing requirements..
8
+
9
+ call "%~dp0\venv\scripts\activate"
10
+
11
+ python -m pip install -U pip
12
+ pip install -r requirements.txt
13
+
14
+ if errorlevel 1 (
15
+ echo.
16
+ echo Requirements installation failed. please remove venv folder and run install.bat again.
17
+ ) else (
18
+ echo.
19
+ echo Requirements installed successfully.
20
+ )
21
+ pause
Install.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ if [ ! -d "venv" ]; then
4
+ echo "Creating virtual environment..."
5
+ python -m venv venv
6
+ fi
7
+
8
+ source venv/bin/activate
9
+
10
+ python -m pip install -U pip
11
+ pip install -r requirements.txt && echo "Requirements installed successfully." || {
12
+ echo ""
13
+ echo "Requirements installation failed. Please remove the venv folder and run the script again."
14
+ deactivate
15
+ exit 1
16
+ }
17
+
18
+ deactivate
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2023 jhj0517
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,12 +1,134 @@
1
- ---
2
- title: Whisper WebUI
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.13.1
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Whisper-WebUI
2
+ A Gradio-based browser interface for [Whisper](https://github.com/openai/whisper). You can use it as an Easy Subtitle Generator!
3
+
4
+ ![screen](https://github.com/user-attachments/assets/caea3afd-a73c-40af-a347-8d57914b1d0f)
5
+
6
+
7
+
8
+ ## Notebook
9
+ If you wish to try this on Colab, you can do it in [here](https://colab.research.google.com/github/jhj0517/Whisper-WebUI/blob/master/notebook/whisper-webui.ipynb)!
10
+
11
+ # Feature
12
+ - Select the Whisper implementation you want to use between :
13
+ - [openai/whisper](https://github.com/openai/whisper)
14
+ - [SYSTRAN/faster-whisper](https://github.com/SYSTRAN/faster-whisper) (used by default)
15
+ - [Vaibhavs10/insanely-fast-whisper](https://github.com/Vaibhavs10/insanely-fast-whisper)
16
+ - Generate subtitles from various sources, including :
17
+ - Files
18
+ - Youtube
19
+ - Microphone
20
+ - Currently supported subtitle formats :
21
+ - SRT
22
+ - WebVTT
23
+ - txt ( only text file without timeline )
24
+ - Speech to Text Translation
25
+ - From other languages to English. ( This is Whisper's end-to-end speech-to-text translation feature )
26
+ - Text to Text Translation
27
+ - Translate subtitle files using Facebook NLLB models
28
+ - Translate subtitle files using DeepL API
29
+ - Pre-processing audio input with [Silero VAD](https://github.com/snakers4/silero-vad).
30
+ - Pre-processing audio input to separate BGM with [UVR](https://github.com/Anjok07/ultimatevocalremovergui).
31
+ - Post-processing with speaker diarization using the [pyannote](https://huggingface.co/pyannote/speaker-diarization-3.1) model.
32
+ - To download the pyannote model, you need to have a Huggingface token and manually accept their terms in the pages below.
33
+ 1. https://huggingface.co/pyannote/speaker-diarization-3.1
34
+ 2. https://huggingface.co/pyannote/segmentation-3.0
35
+
36
+ ### Pipeline Diagram
37
+ ![Transcription Pipeline](https://github.com/user-attachments/assets/1d8c63ac-72a4-4a0b-9db0-e03695dcf088)
38
+
39
+ # Installation and Running
40
+
41
+ - ## Running with Pinokio
42
+
43
+ The app is able to run with [Pinokio](https://github.com/pinokiocomputer/pinokio).
44
+
45
+ 1. Install [Pinokio Software](https://program.pinokio.computer/#/?id=install).
46
+ 2. Open the software and search for Whisper-WebUI and install it.
47
+ 3. Start the Whisper-WebUI and connect to the `http://localhost:7860`.
48
+
49
+ - ## Running with Docker
50
+
51
+ 1. Install and launch [Docker-Desktop](https://www.docker.com/products/docker-desktop/).
52
+
53
+ 2. Git clone the repository
54
+
55
+ ```sh
56
+ git clone https://github.com/jhj0517/Whisper-WebUI.git
57
+ ```
58
+
59
+ 3. Build the image ( Image is about 7GB~ )
60
+
61
+ ```sh
62
+ docker compose build
63
+ ```
64
+
65
+ 4. Run the container
66
+
67
+ ```sh
68
+ docker compose up
69
+ ```
70
+
71
+ 5. Connect to the WebUI with your browser at `http://localhost:7860`
72
+
73
+ If needed, update the [`docker-compose.yaml`](https://github.com/jhj0517/Whisper-WebUI/blob/master/docker-compose.yaml) to match your environment.
74
+
75
+ - ## Run Locally
76
+
77
+ ### Prerequisite
78
+ To run this WebUI, you need to have `git`, `3.10 <= python <= 3.12`, `FFmpeg`. <br>
79
+ And if you're not using an Nvida GPU, or using a different `CUDA` version than 12.4, edit the [`requirements.txt`](https://github.com/jhj0517/Whisper-WebUI/blob/master/requirements.txt) to match your environment.
80
+
81
+ Please follow the links below to install the necessary software:
82
+ - git : [https://git-scm.com/downloads](https://git-scm.com/downloads)
83
+ - python : [https://www.python.org/downloads/](https://www.python.org/downloads/) **`3.10 ~ 3.12` is recommended.**
84
+ - FFmpeg : [https://ffmpeg.org/download.html](https://ffmpeg.org/download.html)
85
+ - CUDA : [https://developer.nvidia.com/cuda-downloads](https://developer.nvidia.com/cuda-downloads)
86
+
87
+ After installing FFmpeg, **make sure to add the `FFmpeg/bin` folder to your system PATH!**
88
+
89
+ ### Installation Using the Script Files
90
+
91
+ 1. git clone this repository
92
+ ```shell
93
+ git clone https://github.com/jhj0517/Whisper-WebUI.git
94
+ ```
95
+ 2. Run `install.bat` or `install.sh` to install dependencies. (It will create a `venv` directory and install dependencies there.)
96
+ 3. Start WebUI with `start-webui.bat` or `start-webui.sh` (It will run `python app.py` after activating the venv)
97
+
98
+ And you can also run the project with command line arguments if you like to, see [wiki](https://github.com/jhj0517/Whisper-WebUI/wiki/Command-Line-Arguments) for a guide to arguments.
99
+
100
+ # VRAM Usages
101
+ This project is integrated with [faster-whisper](https://github.com/guillaumekln/faster-whisper) by default for better VRAM usage and transcription speed.
102
+
103
+ According to faster-whisper, the efficiency of the optimized whisper model is as follows:
104
+ | Implementation | Precision | Beam size | Time | Max. GPU memory | Max. CPU memory |
105
+ |-------------------|-----------|-----------|-------|-----------------|-----------------|
106
+ | openai/whisper | fp16 | 5 | 4m30s | 11325MB | 9439MB |
107
+ | faster-whisper | fp16 | 5 | 54s | 4755MB | 3244MB |
108
+
109
+ If you want to use an implementation other than faster-whisper, use `--whisper_type` arg and the repository name.<br>
110
+ Read [wiki](https://github.com/jhj0517/Whisper-WebUI/wiki/Command-Line-Arguments) for more info about CLI args.
111
+
112
+ If you want to use a fine-tuned model, manually place the models in `models/Whisper/` corresponding to the implementation.
113
+
114
+ Alternatively, if you enter the huggingface repo id (e.g, [deepdml/faster-whisper-large-v3-turbo-ct2](https://huggingface.co/deepdml/faster-whisper-large-v3-turbo-ct2)) in the "Model" dropdown, it will be automatically downloaded in the directory.
115
+
116
+ ![image](https://github.com/user-attachments/assets/76487a46-b0a5-4154-b735-ded73b2d83d4)
117
+
118
+ # REST API
119
+ If you're interested in deploying this app as a REST API, please check out [/backend](https://github.com/jhj0517/Whisper-WebUI/tree/master/backend).
120
+
121
+ ## TODO🗓
122
+
123
+ - [x] Add DeepL API translation
124
+ - [x] Add NLLB Model translation
125
+ - [x] Integrate with faster-whisper
126
+ - [x] Integrate with insanely-fast-whisper
127
+ - [x] Integrate with whisperX ( Only speaker diarization part )
128
+ - [x] Add background music separation pre-processing with [UVR](https://github.com/Anjok07/ultimatevocalremovergui)
129
+ - [x] Add fast api script
130
+ - [ ] Add CLI usages
131
+ - [ ] Support real-time transcription for microphone
132
+
133
+ ### Translation 🌐
134
+ Any PRs that translate the language into [translation.yaml](https://github.com/jhj0517/Whisper-WebUI/blob/master/configs/translation.yaml) would be greatly appreciated!
app.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import gradio as gr
4
+ from gradio_i18n import Translate, gettext as _
5
+ import yaml
6
+
7
+ from modules.utils.paths import (FASTER_WHISPER_MODELS_DIR, DIARIZATION_MODELS_DIR, OUTPUT_DIR, WHISPER_MODELS_DIR,
8
+ INSANELY_FAST_WHISPER_MODELS_DIR, NLLB_MODELS_DIR, DEFAULT_PARAMETERS_CONFIG_PATH,
9
+ UVR_MODELS_DIR, I18N_YAML_PATH)
10
+ from modules.utils.files_manager import load_yaml, MEDIA_EXTENSION
11
+ from modules.whisper.whisper_factory import WhisperFactory
12
+ from modules.translation.nllb_inference import NLLBInference
13
+ from modules.ui.htmls import *
14
+ from modules.utils.cli_manager import str2bool
15
+ from modules.utils.youtube_manager import get_ytmetas
16
+ from modules.translation.deepl_api import DeepLAPI
17
+ from modules.whisper.data_classes import *
18
+
19
+
20
+ class App:
21
+ def __init__(self, args):
22
+ self.args = args
23
+ self.app = gr.Blocks(css=CSS, theme=self.args.theme, delete_cache=(60, 3600))
24
+ self.whisper_inf = WhisperFactory.create_whisper_inference(
25
+ whisper_type=self.args.whisper_type,
26
+ whisper_model_dir=self.args.whisper_model_dir,
27
+ faster_whisper_model_dir=self.args.faster_whisper_model_dir,
28
+ insanely_fast_whisper_model_dir=self.args.insanely_fast_whisper_model_dir,
29
+ uvr_model_dir=self.args.uvr_model_dir,
30
+ output_dir=self.args.output_dir,
31
+ )
32
+ self.nllb_inf = NLLBInference(
33
+ model_dir=self.args.nllb_model_dir,
34
+ output_dir=os.path.join(self.args.output_dir, "translations")
35
+ )
36
+ self.deepl_api = DeepLAPI(
37
+ output_dir=os.path.join(self.args.output_dir, "translations")
38
+ )
39
+ self.i18n = load_yaml(I18N_YAML_PATH)
40
+ self.default_params = load_yaml(DEFAULT_PARAMETERS_CONFIG_PATH)
41
+ print(f"Use \"{self.args.whisper_type}\" implementation\n"
42
+ f"Device \"{self.whisper_inf.device}\" is detected")
43
+
44
+ def create_pipeline_inputs(self):
45
+ whisper_params = self.default_params["whisper"]
46
+ vad_params = self.default_params["vad"]
47
+ diarization_params = self.default_params["diarization"]
48
+ uvr_params = self.default_params["bgm_separation"]
49
+
50
+ with gr.Row():
51
+ dd_model = gr.Dropdown(choices=self.whisper_inf.available_models, value=whisper_params["model_size"],
52
+ label=_("Model"), allow_custom_value=True)
53
+ dd_lang = gr.Dropdown(choices=self.whisper_inf.available_langs + [AUTOMATIC_DETECTION],
54
+ value=AUTOMATIC_DETECTION if whisper_params["lang"] == AUTOMATIC_DETECTION.unwrap()
55
+ else whisper_params["lang"], label=_("Language"))
56
+ dd_file_format = gr.Dropdown(choices=["SRT", "WebVTT", "txt", "LRC"], value=whisper_params["file_format"], label=_("File Format"))
57
+ with gr.Row():
58
+ cb_translate = gr.Checkbox(value=whisper_params["is_translate"], label=_("Translate to English?"),
59
+ interactive=True)
60
+ with gr.Row():
61
+ cb_timestamp = gr.Checkbox(value=whisper_params["add_timestamp"],
62
+ label=_("Add a timestamp to the end of the filename"),
63
+ interactive=True)
64
+
65
+ with gr.Accordion(_("Advanced Parameters"), open=False):
66
+ whisper_inputs = WhisperParams.to_gradio_inputs(defaults=whisper_params, only_advanced=True,
67
+ whisper_type=self.args.whisper_type,
68
+ available_compute_types=self.whisper_inf.available_compute_types,
69
+ compute_type=self.whisper_inf.current_compute_type)
70
+
71
+ with gr.Accordion(_("Background Music Remover Filter"), open=False):
72
+ uvr_inputs = BGMSeparationParams.to_gradio_input(defaults=uvr_params,
73
+ available_models=self.whisper_inf.music_separator.available_models,
74
+ available_devices=self.whisper_inf.music_separator.available_devices,
75
+ device=self.whisper_inf.music_separator.device)
76
+
77
+ with gr.Accordion(_("Voice Detection Filter"), open=False):
78
+ vad_inputs = VadParams.to_gradio_inputs(defaults=vad_params)
79
+
80
+ with gr.Accordion(_("Diarization"), open=False):
81
+ diarization_inputs = DiarizationParams.to_gradio_inputs(defaults=diarization_params,
82
+ available_devices=self.whisper_inf.diarizer.available_device,
83
+ device=self.whisper_inf.diarizer.device)
84
+
85
+ pipeline_inputs = [dd_model, dd_lang, cb_translate] + whisper_inputs + vad_inputs + diarization_inputs + uvr_inputs
86
+
87
+ return (
88
+ pipeline_inputs,
89
+ dd_file_format,
90
+ cb_timestamp
91
+ )
92
+
93
+ def launch(self):
94
+ translation_params = self.default_params["translation"]
95
+ deepl_params = translation_params["deepl"]
96
+ nllb_params = translation_params["nllb"]
97
+ uvr_params = self.default_params["bgm_separation"]
98
+
99
+ with self.app:
100
+ lang = gr.Radio(choices=list(self.i18n.keys()),
101
+ label=_("Language"), interactive=True,
102
+ visible=False, # Set it by development purpose.
103
+ )
104
+ with Translate(I18N_YAML_PATH):
105
+ with gr.Row():
106
+ with gr.Column():
107
+ gr.Markdown(MARKDOWN, elem_id="md_project")
108
+ with gr.Tabs():
109
+ with gr.TabItem(_("File")): # tab1
110
+ with gr.Column():
111
+ input_file = gr.Files(type="filepath", label=_("Upload File here"), file_types=MEDIA_EXTENSION)
112
+ tb_input_folder = gr.Textbox(label="Input Folder Path (Optional)",
113
+ info="Optional: Specify the folder path where the input files are located, if you prefer to use local files instead of uploading them."
114
+ " Leave this field empty if you do not wish to use a local path.",
115
+ visible=self.args.colab,
116
+ value="")
117
+ cb_include_subdirectory = gr.Checkbox(label="Include Subdirectory Files",
118
+ info="When using Input Folder Path above, whether to include all files in the subdirectory or not.",
119
+ visible=self.args.colab,
120
+ value=False)
121
+ cb_save_same_dir = gr.Checkbox(label="Save outputs at same directory",
122
+ info="When using Input Folder Path above, whether to save output in the same directory as inputs or not, in addition to the original"
123
+ " output directory.",
124
+ visible=self.args.colab,
125
+ value=True)
126
+ pipeline_params, dd_file_format, cb_timestamp = self.create_pipeline_inputs()
127
+
128
+ with gr.Row():
129
+ btn_run = gr.Button(_("GENERATE SUBTITLE FILE"), variant="primary")
130
+ with gr.Row():
131
+ tb_indicator = gr.Textbox(label=_("Output"), scale=5)
132
+ files_subtitles = gr.Files(label=_("Downloadable output file"), scale=3, interactive=False)
133
+ btn_openfolder = gr.Button('📂', scale=1)
134
+
135
+ params = [input_file, tb_input_folder, cb_include_subdirectory, cb_save_same_dir,
136
+ dd_file_format, cb_timestamp]
137
+ params = params + pipeline_params
138
+ btn_run.click(fn=self.whisper_inf.transcribe_file,
139
+ inputs=params,
140
+ outputs=[tb_indicator, files_subtitles])
141
+ btn_openfolder.click(fn=lambda: self.open_folder("outputs"), inputs=None, outputs=None)
142
+
143
+ with gr.TabItem(_("Youtube")): # tab2
144
+ with gr.Row():
145
+ tb_youtubelink = gr.Textbox(label=_("Youtube Link"))
146
+ with gr.Row(equal_height=True):
147
+ with gr.Column():
148
+ img_thumbnail = gr.Image(label=_("Youtube Thumbnail"))
149
+ with gr.Column():
150
+ tb_title = gr.Label(label=_("Youtube Title"))
151
+ tb_description = gr.Textbox(label=_("Youtube Description"), max_lines=15)
152
+
153
+ pipeline_params, dd_file_format, cb_timestamp = self.create_pipeline_inputs()
154
+
155
+ with gr.Row():
156
+ btn_run = gr.Button(_("GENERATE SUBTITLE FILE"), variant="primary")
157
+ with gr.Row():
158
+ tb_indicator = gr.Textbox(label=_("Output"), scale=5)
159
+ files_subtitles = gr.Files(label=_("Downloadable output file"), scale=3)
160
+ btn_openfolder = gr.Button('📂', scale=1)
161
+
162
+ params = [tb_youtubelink, dd_file_format, cb_timestamp]
163
+
164
+ btn_run.click(fn=self.whisper_inf.transcribe_youtube,
165
+ inputs=params + pipeline_params,
166
+ outputs=[tb_indicator, files_subtitles])
167
+ tb_youtubelink.change(get_ytmetas, inputs=[tb_youtubelink],
168
+ outputs=[img_thumbnail, tb_title, tb_description])
169
+ btn_openfolder.click(fn=lambda: self.open_folder("outputs"), inputs=None, outputs=None)
170
+
171
+ with gr.TabItem(_("Mic")): # tab3
172
+ with gr.Row():
173
+ mic_input = gr.Microphone(label=_("Record with Mic"), type="filepath", interactive=True,
174
+ show_download_button=True)
175
+
176
+ pipeline_params, dd_file_format, cb_timestamp = self.create_pipeline_inputs()
177
+
178
+ with gr.Row():
179
+ btn_run = gr.Button(_("GENERATE SUBTITLE FILE"), variant="primary")
180
+ with gr.Row():
181
+ tb_indicator = gr.Textbox(label=_("Output"), scale=5)
182
+ files_subtitles = gr.Files(label=_("Downloadable output file"), scale=3)
183
+ btn_openfolder = gr.Button('📂', scale=1)
184
+
185
+ params = [mic_input, dd_file_format, cb_timestamp]
186
+
187
+ btn_run.click(fn=self.whisper_inf.transcribe_mic,
188
+ inputs=params + pipeline_params,
189
+ outputs=[tb_indicator, files_subtitles])
190
+ btn_openfolder.click(fn=lambda: self.open_folder("outputs"), inputs=None, outputs=None)
191
+
192
+ with gr.TabItem(_("T2T Translation")): # tab 4
193
+ with gr.Row():
194
+ file_subs = gr.Files(type="filepath", label=_("Upload Subtitle Files to translate here"))
195
+
196
+ with gr.TabItem(_("DeepL API")): # sub tab1
197
+ with gr.Row():
198
+ tb_api_key = gr.Textbox(label=_("Your Auth Key (API KEY)"),
199
+ value=deepl_params["api_key"])
200
+ with gr.Row():
201
+ dd_source_lang = gr.Dropdown(label=_("Source Language"),
202
+ value=AUTOMATIC_DETECTION if deepl_params["source_lang"] == AUTOMATIC_DETECTION.unwrap()
203
+ else deepl_params["source_lang"],
204
+ choices=list(self.deepl_api.available_source_langs.keys()))
205
+ dd_target_lang = gr.Dropdown(label=_("Target Language"),
206
+ value=deepl_params["target_lang"],
207
+ choices=list(self.deepl_api.available_target_langs.keys()))
208
+ with gr.Row():
209
+ cb_is_pro = gr.Checkbox(label=_("Pro User?"), value=deepl_params["is_pro"])
210
+ with gr.Row():
211
+ cb_timestamp = gr.Checkbox(value=translation_params["add_timestamp"],
212
+ label=_("Add a timestamp to the end of the filename"),
213
+ interactive=True)
214
+ with gr.Row():
215
+ btn_run = gr.Button(_("TRANSLATE SUBTITLE FILE"), variant="primary")
216
+ with gr.Row():
217
+ tb_indicator = gr.Textbox(label=_("Output"), scale=5)
218
+ files_subtitles = gr.Files(label=_("Downloadable output file"), scale=3)
219
+ btn_openfolder = gr.Button('📂', scale=1)
220
+
221
+ btn_run.click(fn=self.deepl_api.translate_deepl,
222
+ inputs=[tb_api_key, file_subs, dd_source_lang, dd_target_lang,
223
+ cb_is_pro, cb_timestamp],
224
+ outputs=[tb_indicator, files_subtitles])
225
+
226
+ btn_openfolder.click(
227
+ fn=lambda: self.open_folder(os.path.join(self.args.output_dir, "translations")),
228
+ inputs=None,
229
+ outputs=None)
230
+
231
+ with gr.TabItem(_("NLLB")): # sub tab2
232
+ with gr.Row():
233
+ dd_model_size = gr.Dropdown(label=_("Model"), value=nllb_params["model_size"],
234
+ choices=self.nllb_inf.available_models)
235
+ dd_source_lang = gr.Dropdown(label=_("Source Language"),
236
+ value=nllb_params["source_lang"],
237
+ choices=self.nllb_inf.available_source_langs)
238
+ dd_target_lang = gr.Dropdown(label=_("Target Language"),
239
+ value=nllb_params["target_lang"],
240
+ choices=self.nllb_inf.available_target_langs)
241
+ with gr.Row():
242
+ nb_max_length = gr.Number(label="Max Length Per Line", value=nllb_params["max_length"],
243
+ precision=0)
244
+ with gr.Row():
245
+ cb_timestamp = gr.Checkbox(value=translation_params["add_timestamp"],
246
+ label=_("Add a timestamp to the end of the filename"),
247
+ interactive=True)
248
+ with gr.Row():
249
+ btn_run = gr.Button(_("TRANSLATE SUBTITLE FILE"), variant="primary")
250
+ with gr.Row():
251
+ tb_indicator = gr.Textbox(label=_("Output"), scale=5)
252
+ files_subtitles = gr.Files(label=_("Downloadable output file"), scale=3)
253
+ btn_openfolder = gr.Button('📂', scale=1)
254
+ with gr.Column():
255
+ md_vram_table = gr.HTML(NLLB_VRAM_TABLE, elem_id="md_nllb_vram_table")
256
+
257
+ btn_run.click(fn=self.nllb_inf.translate_file,
258
+ inputs=[file_subs, dd_model_size, dd_source_lang, dd_target_lang,
259
+ nb_max_length, cb_timestamp],
260
+ outputs=[tb_indicator, files_subtitles])
261
+
262
+ btn_openfolder.click(
263
+ fn=lambda: self.open_folder(os.path.join(self.args.output_dir, "translations")),
264
+ inputs=None,
265
+ outputs=None)
266
+
267
+ with gr.TabItem(_("BGM Separation")):
268
+ files_audio = gr.Files(type="filepath", label=_("Upload Audio Files to separate background music"))
269
+ dd_uvr_device = gr.Dropdown(label=_("Device"), value=self.whisper_inf.music_separator.device,
270
+ choices=self.whisper_inf.music_separator.available_devices)
271
+ dd_uvr_model_size = gr.Dropdown(label=_("Model"), value=uvr_params["uvr_model_size"],
272
+ choices=self.whisper_inf.music_separator.available_models)
273
+ nb_uvr_segment_size = gr.Number(label="Segment Size", value=uvr_params["segment_size"],
274
+ precision=0)
275
+ cb_uvr_save_file = gr.Checkbox(label=_("Save separated files to output"),
276
+ value=True, visible=False)
277
+ btn_run = gr.Button(_("SEPARATE BACKGROUND MUSIC"), variant="primary")
278
+ with gr.Column():
279
+ with gr.Row():
280
+ ad_instrumental = gr.Audio(label=_("Instrumental"), scale=8)
281
+ btn_open_instrumental_folder = gr.Button('📂', scale=1)
282
+ with gr.Row():
283
+ ad_vocals = gr.Audio(label=_("Vocals"), scale=8)
284
+ btn_open_vocals_folder = gr.Button('📂', scale=1)
285
+
286
+ btn_run.click(fn=self.whisper_inf.music_separator.separate_files,
287
+ inputs=[files_audio, dd_uvr_model_size, dd_uvr_device, nb_uvr_segment_size,
288
+ cb_uvr_save_file],
289
+ outputs=[ad_instrumental, ad_vocals])
290
+ btn_open_instrumental_folder.click(inputs=None,
291
+ outputs=None,
292
+ fn=lambda: self.open_folder(os.path.join(
293
+ self.args.output_dir, "UVR", "instrumental"
294
+ )))
295
+ btn_open_vocals_folder.click(inputs=None,
296
+ outputs=None,
297
+ fn=lambda: self.open_folder(os.path.join(
298
+ self.args.output_dir, "UVR", "vocals"
299
+ )))
300
+
301
+ # Launch the app with optional gradio settings
302
+ args = self.args
303
+ self.app.queue(
304
+ api_open=args.api_open
305
+ ).launch(
306
+ share=args.share,
307
+ server_name=args.server_name,
308
+ server_port=args.server_port,
309
+ auth=(args.username, args.password) if args.username and args.password else None,
310
+ root_path=args.root_path,
311
+ inbrowser=args.inbrowser,
312
+ ssl_verify=args.ssl_verify,
313
+ ssl_keyfile=args.ssl_keyfile,
314
+ ssl_keyfile_password=args.ssl_keyfile_password,
315
+ ssl_certfile=args.ssl_certfile,
316
+ allowed_paths=eval(args.allowed_paths) if args.allowed_paths else None
317
+ )
318
+
319
+ @staticmethod
320
+ def open_folder(folder_path: str):
321
+ if os.path.exists(folder_path):
322
+ os.system(f"start {folder_path}")
323
+ else:
324
+ os.makedirs(folder_path, exist_ok=True)
325
+ print(f"The directory path {folder_path} has newly created.")
326
+
327
+
328
+ parser = argparse.ArgumentParser()
329
+ parser.add_argument('--whisper_type', type=str, default=WhisperImpl.FASTER_WHISPER.value,
330
+ choices=[item.value for item in WhisperImpl],
331
+ help='A type of the whisper implementation (Github repo name)')
332
+ parser.add_argument('--share', type=str2bool, default=False, nargs='?', const=True, help='Gradio share value')
333
+ parser.add_argument('--server_name', type=str, default=None, help='Gradio server host')
334
+ parser.add_argument('--server_port', type=int, default=None, help='Gradio server port')
335
+ parser.add_argument('--root_path', type=str, default=None, help='Gradio root path')
336
+ parser.add_argument('--username', type=str, default=None, help='Gradio authentication username')
337
+ parser.add_argument('--password', type=str, default=None, help='Gradio authentication password')
338
+ parser.add_argument('--theme', type=str, default=None, help='Gradio Blocks theme')
339
+ parser.add_argument('--colab', type=str2bool, default=False, nargs='?', const=True, help='Is colab user or not')
340
+ parser.add_argument('--api_open', type=str2bool, default=False, nargs='?', const=True,
341
+ help='Enable api or not in Gradio')
342
+ parser.add_argument('--allowed_paths', type=str, default=None, help='Gradio allowed paths')
343
+ parser.add_argument('--inbrowser', type=str2bool, default=True, nargs='?', const=True,
344
+ help='Whether to automatically start Gradio app or not')
345
+ parser.add_argument('--ssl_verify', type=str2bool, default=True, nargs='?', const=True,
346
+ help='Whether to verify SSL or not')
347
+ parser.add_argument('--ssl_keyfile', type=str, default=None, help='SSL Key file location')
348
+ parser.add_argument('--ssl_keyfile_password', type=str, default=None, help='SSL Key file password')
349
+ parser.add_argument('--ssl_certfile', type=str, default=None, help='SSL cert file location')
350
+ parser.add_argument('--whisper_model_dir', type=str, default=WHISPER_MODELS_DIR,
351
+ help='Directory path of the whisper model')
352
+ parser.add_argument('--faster_whisper_model_dir', type=str, default=FASTER_WHISPER_MODELS_DIR,
353
+ help='Directory path of the faster-whisper model')
354
+ parser.add_argument('--insanely_fast_whisper_model_dir', type=str,
355
+ default=INSANELY_FAST_WHISPER_MODELS_DIR,
356
+ help='Directory path of the insanely-fast-whisper model')
357
+ parser.add_argument('--diarization_model_dir', type=str, default=DIARIZATION_MODELS_DIR,
358
+ help='Directory path of the diarization model')
359
+ parser.add_argument('--nllb_model_dir', type=str, default=NLLB_MODELS_DIR,
360
+ help='Directory path of the Facebook NLLB model')
361
+ parser.add_argument('--uvr_model_dir', type=str, default=UVR_MODELS_DIR,
362
+ help='Directory path of the UVR model')
363
+ parser.add_argument('--output_dir', type=str, default=OUTPUT_DIR, help='Directory path of the outputs')
364
+ _args = parser.parse_args()
365
+
366
+ if __name__ == "__main__":
367
+ app = App(args=_args)
368
+ app.launch()
backend/Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM debian:bookworm-slim AS builder
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y curl git python3 python3-pip python3-venv && \
5
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* && \
6
+ mkdir -p /Whisper-WebUI
7
+
8
+ WORKDIR /Whisper-WebUI
9
+
10
+ COPY backend/ backend/
11
+ COPY requirements.txt requirements.txt
12
+
13
+ RUN python3 -m venv venv && \
14
+ . venv/bin/activate && \
15
+ pip install -U -r backend/requirements-backend.txt
16
+
17
+
18
+ FROM debian:bookworm-slim AS runtime
19
+
20
+ RUN apt-get update && \
21
+ apt-get install -y curl ffmpeg python3 && \
22
+ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
23
+
24
+ WORKDIR /Whisper-WebUI
25
+
26
+ COPY . .
27
+ COPY --from=builder /Whisper-WebUI/venv /Whisper-WebUI/venv
28
+
29
+ VOLUME [ "/Whisper-WebUI/models" ]
30
+ VOLUME [ "/Whisper-WebUI/outputs" ]
31
+ VOLUME [ "/Whisper-WebUI/backend" ]
32
+
33
+ ENV PATH="/Whisper-WebUI/venv/bin:$PATH"
34
+ ENV LD_LIBRARY_PATH=/Whisper-WebUI/venv/lib64/python3.11/site-packages/nvidia/cublas/lib:/Whisper-WebUI/venv/lib64/python3.11/site-packages/nvidia/cudnn/lib
35
+
36
+ ENTRYPOINT ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/README.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Whisper-WebUI REST API
2
+ REST API for Whisper-WebUI. Documentation is auto-generated upon deploying the app.
3
+ <br>[Swagger UI](https://github.com/swagger-api/swagger-ui) is available at `app/docs` or root URL with redirection. [Redoc](https://github.com/Redocly/redoc) is available at `app/redoc`.
4
+
5
+ # Setup and Installation
6
+
7
+ Installation assumes that you are in the root directory of Whisper-WebUI
8
+
9
+ 1. Create `.env` in `backend/configs/.env`
10
+ ```
11
+ HF_TOKEN="YOUR_HF_TOKEN FOR DIARIZATION MODEL (READ PERMISSION)"
12
+ DB_URL="sqlite:///backend/records.db"
13
+ ```
14
+ `HF_TOKEN` is used to download diarization model, `DB_URL` indicates where your db file is located. It is stored in `backend/` by default.
15
+
16
+ 2. Install dependency
17
+ ```
18
+ pip install -r backend/requirements-backend.txt
19
+ ```
20
+
21
+ 3. Deploy the server with `uvicorn` or whatever.
22
+ ```
23
+ uvicorn backend.main:app --host 0.0.0.0 --port 8000
24
+ ```
25
+
26
+ ### Deploy with your domain name
27
+ You can deploy the server with your domain name by setting up a reverse proxy with Nginx.
28
+
29
+ 1. Install Nginx if you don't already have it.
30
+ - Linux : https://nginx.org/en/docs/install.html
31
+ - Windows : https://nginx.org/en/docs/windows.html
32
+
33
+ 2. Edit [`nginx.conf`](https://github.com/jhj0517/Whisper-WebUI/blob/master/backend/nginx/nginx.conf) for your domain name.
34
+ https://github.com/jhj0517/Whisper-WebUI/blob/895cafe400944396ad8be5b1cc793b54fecc8bbe/backend/nginx/nginx.conf#L12
35
+
36
+ 3. Add an A type record of your public IPv4 address in your domain provider. (you can get it by searching "What is my IP" in Google)
37
+
38
+ 4. Open a terminal and go to the location of [`nginx.conf`](https://github.com/jhj0517/Whisper-WebUI/blob/master/backend/nginx/nginx.conf), then start the nginx server, so that you can manage nginx-related logs there.
39
+ ```shell
40
+ cd backend/nginx
41
+ nginx -c "/path/to/Whisper-WebUI/backend/nginx/nginx.conf"
42
+ ```
43
+
44
+ 5. Open another terminal in the root project location `/Whisper-WebUI`, and deploy the app with `uvicorn` or whatever. Now the app will be available at your domain.
45
+ ```shell
46
+ uvicorn backend.main:app --host 0.0.0.0 --port 8000
47
+ ```
48
+
49
+ 6. When you turn off nginx, you can use `nginx -s stop`.
50
+ ```shell
51
+ cd backend/nginx
52
+ nginx -s stop -c "/path/to/Whisper-WebUI/backend/nginx/nginx.conf"
53
+ ```
54
+
55
+
56
+ ## Configuration
57
+ You can set some server configurations in [config.yaml](https://github.com/jhj0517/Whisper-WebUI/blob/master/backend/configs/config.yaml).
58
+ <br>For example, initial model size for Whisper or the cleanup frequency and TTL for cached files.
59
+ <br>If the endpoint generates and saves the file, all output files are stored in the `cache` directory, e.g. separated vocal/instrument files for `/bgm-separation` are saved in `cache` directory.
60
+
61
+ ## Docker
62
+ The Dockerfile should be built when you're in the root directory of Whisper-WebUI.
63
+
64
+ 1. git clone this repository
65
+ ```
66
+ git clone https://github.com/jhj0517/Whisper-WebUI.git
67
+ ```
68
+ 2. Mount volume paths with your local paths in `docker-compose.yaml`
69
+ https://github.com/jhj0517/Whisper-WebUI/blob/1dd708ec3844dbf0c1f77de9ef5764e883dd4c78/backend/docker-compose.yaml#L12-L15
70
+ 3. Build the image
71
+ ```
72
+ docker compose -f backend/docker-compose.yaml build
73
+ ```
74
+ 4. Run the container
75
+ ```
76
+ docker compose -f backend/docker-compose.yaml up
77
+ ```
78
+
79
+ 5. Then you can read docs at `localhost:8000` (default port is set to `8000` in `docker-compose.yaml`) and run your own tests.
80
+
81
+
82
+ # Architecture
83
+
84
+ ![diagram](https://github.com/user-attachments/assets/37d2ab2d-4eb4-4513-bb7b-027d0d631971)
85
+
86
+ The response can be obtained through [the polling API](https://docs.oracle.com/en/cloud/saas/marketing/responsys-develop/API/REST/Async/asyncApi-v1.3-requests-requestId-get.htm).
87
+ Each task is stored in the DB whenever the task is queued or updated by the process.
88
+
89
+ When the client first sends the `POST` request, the server returns an `identifier` to the client that can be used to track the status of the task. The task status is updated by the processes, and once the task is completed, the client can finally obtain the result.
90
+
91
+ The client needs to implement manual API polling to do this, this is the example for the python client:
92
+ ```python
93
+ def wait_for_task_completion(identifier: str,
94
+ max_attempts: int = 20,
95
+ frequency: int = 3) -> httpx.Response:
96
+ """
97
+ Polls the task status every `frequency` until it is completed, failed, or the `max_attempts` are reached.
98
+ """
99
+ attempts = 0
100
+ while attempts < max_attempts:
101
+ task = fetch_task(identifier)
102
+ status = task.json()["status"]
103
+ if status == "COMPLETED":
104
+ return task["result"]
105
+ if status == "FAILED":
106
+ raise Exception("Task polling failed")
107
+ time.sleep(frequency)
108
+ attempts += 1
109
+ return None
110
+ ```
backend/__init__.py ADDED
File without changes
backend/cache/cached_files_are_generated_here ADDED
File without changes
backend/common/audio.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ import numpy as np
3
+ import httpx
4
+ import faster_whisper
5
+ from pydantic import BaseModel
6
+ from fastapi import (
7
+ HTTPException,
8
+ UploadFile,
9
+ )
10
+ from typing import Annotated, Any, BinaryIO, Literal, Generator, Union, Optional, List, Tuple
11
+
12
+
13
+ class AudioInfo(BaseModel):
14
+ duration: float
15
+
16
+
17
+ async def read_audio(
18
+ file: Optional[UploadFile] = None,
19
+ file_url: Optional[str] = None
20
+ ):
21
+ """Read audio from "UploadFile". This resamples sampling rates to 16000."""
22
+ if (file and file_url) or (not file and not file_url):
23
+ raise HTTPException(status_code=400, detail="Provide only one of file or file_url")
24
+
25
+ if file:
26
+ file_content = await file.read()
27
+ elif file_url:
28
+ async with httpx.AsyncClient() as client:
29
+ file_response = await client.get(file_url)
30
+ if file_response.status_code != 200:
31
+ raise HTTPException(status_code=422, detail="Could not download the file")
32
+ file_content = file_response.content
33
+ file_bytes = BytesIO(file_content)
34
+ audio = faster_whisper.audio.decode_audio(file_bytes)
35
+ duration = len(audio) / 16000
36
+ return audio, AudioInfo(duration=duration)
backend/common/cache_manager.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import os
3
+ from typing import Optional
4
+
5
+ from modules.utils.paths import BACKEND_CACHE_DIR
6
+
7
+
8
+ def cleanup_old_files(cache_dir: str = BACKEND_CACHE_DIR, ttl: int = 60):
9
+ now = time.time()
10
+ place_holder_name = "cached_files_are_generated_here"
11
+ for root, dirs, files in os.walk(cache_dir):
12
+ for filename in files:
13
+ if filename == place_holder_name:
14
+ continue
15
+ filepath = os.path.join(root, filename)
16
+ if now - os.path.getmtime(filepath) > ttl:
17
+ try:
18
+ os.remove(filepath)
19
+ except Exception as e:
20
+ print(f"Error removing {filepath}")
21
+ raise
backend/common/compresser.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import zipfile
3
+ from typing import List, Optional
4
+ import hashlib
5
+
6
+
7
+ def compress_files(file_paths: List[str], output_zip_path: str) -> str:
8
+ """
9
+ Compress multiple files into a single zip file.
10
+
11
+ Args:
12
+ file_paths (List[str]): List of paths to files to be compressed.
13
+ output_zip (str): Path and name of the output zip file.
14
+
15
+ Raises:
16
+ FileNotFoundError: If any of the input files doesn't exist.
17
+ """
18
+ os.makedirs(os.path.dirname(output_zip_path), exist_ok=True)
19
+ compression = zipfile.ZIP_DEFLATED
20
+
21
+ with zipfile.ZipFile(output_zip_path, 'w', compression=compression) as zipf:
22
+ for file_path in file_paths:
23
+ if not os.path.exists(file_path):
24
+ raise FileNotFoundError(f"File not found: {file_path}")
25
+
26
+ file_name = os.path.basename(file_path)
27
+ zipf.write(file_path, file_name)
28
+ return output_zip_path
29
+
30
+
31
+ def get_file_hash(file_path: str) -> str:
32
+ """Generate the hash of a file using the specified hashing algorithm. It generates hash by content not path. """
33
+ hash_func = hashlib.new("sha256")
34
+ try:
35
+ with open(file_path, 'rb') as f:
36
+ for chunk in iter(lambda: f.read(4096), b""):
37
+ hash_func.update(chunk)
38
+ return hash_func.hexdigest()
39
+ except FileNotFoundError:
40
+ return f"File not found: {file_path}"
41
+ except Exception as e:
42
+ return f"An error occurred: {str(e)}"
43
+
44
+
45
+ def find_file_by_hash(dir_path: str, hash_str: str) -> Optional[str]:
46
+ """Get file path from the directory based on its hash"""
47
+ if not os.path.exists(dir_path) and os.path.isdir(dir_path):
48
+ raise ValueError(f"Directory {dir_path} does not exist")
49
+
50
+ files = [os.path.join(dir_path, f) for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f))]
51
+
52
+ for f in files:
53
+ f_hash = get_file_hash(f)
54
+ if hash_str == f_hash:
55
+ return f
56
+ return None
57
+
58
+
backend/common/config_loader.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ from modules.utils.paths import SERVER_CONFIG_PATH, SERVER_DOTENV_PATH
4
+ from modules.utils.files_manager import load_yaml, save_yaml
5
+
6
+ import functools
7
+
8
+
9
+ @functools.lru_cache
10
+ def load_server_config(config_path: str = SERVER_CONFIG_PATH) -> dict:
11
+ if os.getenv("TEST_ENV", "false").lower() == "true":
12
+ server_config = load_yaml(config_path)
13
+ server_config["whisper"]["model_size"] = "tiny"
14
+ server_config["whisper"]["compute_type"] = "float32"
15
+ save_yaml(server_config, config_path)
16
+
17
+ return load_yaml(config_path)
18
+
19
+
20
+ @functools.lru_cache
21
+ def read_env(key: str, default: str = None, dotenv_path: str = SERVER_DOTENV_PATH):
22
+ load_dotenv(dotenv_path)
23
+ value = os.getenv(key, default)
24
+ return value
25
+
backend/common/models.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, validator
2
+ from typing import List, Any, Optional
3
+ from backend.db.task.models import TaskStatus, ResultType, TaskType
4
+
5
+
6
+ class QueueResponse(BaseModel):
7
+ identifier: str = Field(..., description="Unique identifier for the queued task that can be used for tracking")
8
+ status: TaskStatus = Field(..., description="Current status of the task")
9
+ message: str = Field(..., description="Message providing additional information about the task")
10
+
11
+
12
+ class Response(BaseModel):
13
+ identifier: str
14
+ message: str
backend/configs/config.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ whisper:
2
+ # Default implementation is faster-whisper. This indicates model name within `models\Whisper\faster-whisper`
3
+ model_size: large-v2
4
+ # Compute type. 'float16' for CUDA, 'float32' for CPU.
5
+ compute_type: float16
6
+
7
+ bgm_separation:
8
+ # UVR model sizes between ["UVR-MDX-NET-Inst_HQ_4", "UVR-MDX-NET-Inst_3"]
9
+ model_size: UVR-MDX-NET-Inst_HQ_4
10
+ # Whether to offload the model after the inference. Should be true if your setup has a VRAM less than <16GB
11
+ enable_offload: true
12
+ # Device to load BGM separation model
13
+ device: cuda
14
+
15
+ # Settings that apply to the `cache' directory. The output files for `/bgm-separation` are stored in the `cache' directory,
16
+ # (You can check out the actual generated files by testing `/bgm-separation`.)
17
+ # You can adjust the TTL/cleanup frequency of the files in the `cache' directory here.
18
+ cache:
19
+ # TTL (Time-To-Live) in seconds, defaults to 10 minutes
20
+ ttl: 600
21
+ # Clean up frequency in seconds, defaults to 1 minutes
22
+ frequency: 60
23
+
backend/db/__init__.py ADDED
File without changes
backend/db/db_instance.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import os
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.orm import sessionmaker
5
+ from functools import wraps
6
+ from sqlalchemy.exc import SQLAlchemyError
7
+ from fastapi import HTTPException
8
+ from sqlmodel import SQLModel
9
+ from dotenv import load_dotenv
10
+
11
+ from backend.common.config_loader import read_env
12
+
13
+
14
+ @functools.lru_cache
15
+ def init_db():
16
+ db_url = read_env("DB_URL", "sqlite:///backend/records.db")
17
+ engine = create_engine(db_url, connect_args={"check_same_thread": False})
18
+ SQLModel.metadata.create_all(engine)
19
+ return sessionmaker(autocommit=False, autoflush=False, bind=engine)
20
+
21
+
22
+ def get_db_session():
23
+ db_instance = init_db()
24
+ return db_instance()
25
+
26
+
27
+ def handle_database_errors(func):
28
+ @wraps(func)
29
+ def wrapper(*args, **kwargs):
30
+ session = None
31
+ try:
32
+ session = get_db_session()
33
+ kwargs['session'] = session
34
+
35
+ return func(*args, **kwargs)
36
+ except Exception as e:
37
+ print(f"Database error has occurred: {e}")
38
+ raise
39
+ finally:
40
+ if session:
41
+ session.close()
42
+ return wrapper
backend/db/task/__init__.py ADDED
File without changes
backend/db/task/dao.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from sqlalchemy.orm import Session
3
+ from fastapi import Depends
4
+
5
+ from ..db_instance import handle_database_errors, get_db_session
6
+ from .models import Task, TasksResult, TaskStatus
7
+
8
+
9
+ @handle_database_errors
10
+ def add_task_to_db(
11
+ session,
12
+ status=TaskStatus.QUEUED,
13
+ task_type=None,
14
+ language=None,
15
+ task_params=None,
16
+ file_name=None,
17
+ url=None,
18
+ audio_duration=None,
19
+ ):
20
+ """
21
+ Add task to the db
22
+ """
23
+ task = Task(
24
+ status=status,
25
+ language=language,
26
+ file_name=file_name,
27
+ url=url,
28
+ task_type=task_type,
29
+ task_params=task_params,
30
+ audio_duration=audio_duration,
31
+ )
32
+ session.add(task)
33
+ session.commit()
34
+ return task.uuid
35
+
36
+
37
+ @handle_database_errors
38
+ def update_task_status_in_db(
39
+ identifier: str,
40
+ update_data: Dict[str, Any],
41
+ session: Session,
42
+ ):
43
+ """
44
+ Update task status and attributes in the database.
45
+
46
+ Args:
47
+ identifier (str): Identifier of the task to be updated.
48
+ update_data (Dict[str, Any]): Dictionary containing the attributes to update along with their new values.
49
+ session (Session, optional): Database session. Defaults to Depends(get_db_session).
50
+
51
+ Returns:
52
+ None
53
+ """
54
+ task = session.query(Task).filter_by(uuid=identifier).first()
55
+ if task:
56
+ for key, value in update_data.items():
57
+ setattr(task, key, value)
58
+ session.commit()
59
+
60
+
61
+ @handle_database_errors
62
+ def get_task_status_from_db(
63
+ identifier: str, session: Session
64
+ ):
65
+ """Retrieve task status from db"""
66
+ task = session.query(Task).filter(Task.uuid == identifier).first()
67
+ if task:
68
+ return task
69
+ else:
70
+ return None
71
+
72
+
73
+ @handle_database_errors
74
+ def get_all_tasks_status_from_db(session: Session):
75
+ """Get all tasks from db"""
76
+ columns = [Task.uuid, Task.status, Task.task_type]
77
+ query = session.query(*columns)
78
+ tasks = [task for task in query]
79
+ return TasksResult(tasks=tasks)
80
+
81
+
82
+ @handle_database_errors
83
+ def delete_task_from_db(identifier: str, session: Session):
84
+ """Delete task from db"""
85
+ task = session.query(Task).filter(Task.uuid == identifier).first()
86
+
87
+ if task:
88
+ # If the task exists, delete it from the database
89
+ session.delete(task)
90
+ session.commit()
91
+ return True
92
+ else:
93
+ # If the task does not exist, return False
94
+ return False
backend/db/task/models.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ported from https://github.com/pavelzbornik/whisperX-FastAPI/blob/main/app/models.py
2
+
3
+ from enum import Enum
4
+ from pydantic import BaseModel
5
+ from typing import Optional, List
6
+ from uuid import uuid4
7
+ from datetime import datetime
8
+ from sqlalchemy.types import Enum as SQLAlchemyEnum
9
+ from typing import Any
10
+ from sqlmodel import SQLModel, Field, JSON, Column
11
+
12
+
13
+ class ResultType(str, Enum):
14
+ JSON = "json"
15
+ FILEPATH = "filepath"
16
+
17
+
18
+ class TaskStatus(str, Enum):
19
+ PENDING = "pending"
20
+ IN_PROGRESS = "in_progress"
21
+ COMPLETED = "completed"
22
+ FAILED = "failed"
23
+ CANCELLED = "cancelled"
24
+ QUEUED = "queued"
25
+ PAUSED = "paused"
26
+ RETRYING = "retrying"
27
+
28
+ def __str__(self):
29
+ return self.value
30
+
31
+
32
+ class TaskType(str, Enum):
33
+ TRANSCRIPTION = "transcription"
34
+ VAD = "vad"
35
+ BGM_SEPARATION = "bgm_separation"
36
+
37
+ def __str__(self):
38
+ return self.value
39
+
40
+
41
+ class TaskStatusResponse(BaseModel):
42
+ """`TaskStatusResponse` is a wrapper class that hides sensitive information from `Task`"""
43
+ identifier: str = Field(..., description="Unique identifier for the queued task that can be used for tracking")
44
+ status: TaskStatus = Field(..., description="Current status of the task")
45
+ task_type: Optional[TaskType] = Field(
46
+ default=None,
47
+ description="Type/category of the task"
48
+ )
49
+ result_type: Optional[ResultType] = Field(
50
+ default=ResultType.JSON,
51
+ description="Result type whether it's a filepath or JSON"
52
+ )
53
+ result: Optional[Any] = Field(
54
+ default=None,
55
+ description="JSON data representing the result of the task"
56
+ )
57
+ task_params: Optional[dict] = Field(
58
+ default=None,
59
+ description="Parameters of the task"
60
+ )
61
+ error: Optional[str] = Field(
62
+ default=None,
63
+ description="Error message, if any, associated with the task"
64
+ )
65
+ duration: Optional[float] = Field(
66
+ default=None,
67
+ description="Duration of the task execution"
68
+ )
69
+
70
+
71
+ class Task(SQLModel, table=True):
72
+ """
73
+ Table to store tasks information.
74
+
75
+ Attributes:
76
+ - id: Unique identifier for each task (Primary Key).
77
+ - uuid: Universally unique identifier for each task.
78
+ - status: Current status of the task.
79
+ - result: JSON data representing the result of the task.
80
+ - result_type: Type of the data whether it is normal JSON data or filepath.
81
+ - file_name: Name of the file associated with the task.
82
+ - task_type: Type/category of the task.
83
+ - duration: Duration of the task execution.
84
+ - error: Error message, if any, associated with the task.
85
+ - created_at: Date and time of creation.
86
+ - updated_at: Date and time of last update.
87
+ """
88
+
89
+ __tablename__ = "tasks"
90
+
91
+ id: Optional[int] = Field(
92
+ default=None,
93
+ primary_key=True,
94
+ description="Unique identifier for each task (Primary Key)"
95
+ )
96
+ uuid: str = Field(
97
+ default_factory=lambda: str(uuid4()),
98
+ description="Universally unique identifier for each task"
99
+ )
100
+ status: Optional[TaskStatus] = Field(
101
+ default=None,
102
+ sa_column=Field(sa_column=SQLAlchemyEnum(TaskStatus)),
103
+ description="Current status of the task",
104
+ )
105
+ result: Optional[dict] = Field(
106
+ default_factory=dict,
107
+ sa_column=Column(JSON),
108
+ description="JSON data representing the result of the task"
109
+ )
110
+ result_type: Optional[ResultType] = Field(
111
+ default=ResultType.JSON,
112
+ sa_column=Field(sa_column=SQLAlchemyEnum(ResultType)),
113
+ description="Result type whether it's a filepath or JSON"
114
+ )
115
+ file_name: Optional[str] = Field(
116
+ default=None,
117
+ description="Name of the file associated with the task"
118
+ )
119
+ url: Optional[str] = Field(
120
+ default=None,
121
+ description="URL of the file associated with the task"
122
+ )
123
+ audio_duration: Optional[float] = Field(
124
+ default=None,
125
+ description="Duration of the audio in seconds"
126
+ )
127
+ language: Optional[str] = Field(
128
+ default=None,
129
+ description="Language of the file associated with the task"
130
+ )
131
+ task_type: Optional[TaskType] = Field(
132
+ default=None,
133
+ sa_column=Field(sa_column=SQLAlchemyEnum(TaskType)),
134
+ description="Type/category of the task"
135
+ )
136
+ task_params: Optional[dict] = Field(
137
+ default_factory=dict,
138
+ sa_column=Column(JSON),
139
+ description="Parameters of the task"
140
+ )
141
+ duration: Optional[float] = Field(
142
+ default=None,
143
+ description="Duration of the task execution"
144
+ )
145
+ error: Optional[str] = Field(
146
+ default=None,
147
+ description="Error message, if any, associated with the task"
148
+ )
149
+ created_at: datetime = Field(
150
+ default_factory=datetime.utcnow,
151
+ description="Date and time of creation"
152
+ )
153
+ updated_at: datetime = Field(
154
+ default_factory=datetime.utcnow,
155
+ sa_column_kwargs={"onupdate": datetime.utcnow},
156
+ description="Date and time of last update"
157
+ )
158
+
159
+ def to_response(self) -> "TaskStatusResponse":
160
+ return TaskStatusResponse(
161
+ identifier=self.uuid,
162
+ status=self.status,
163
+ task_type=self.task_type,
164
+ result_type=self.result_type,
165
+ result=self.result,
166
+ task_params=self.task_params,
167
+ error=self.error,
168
+ duration=self.duration
169
+ )
170
+
171
+
172
+ class TasksResult(BaseModel):
173
+ tasks: List[Task]
174
+
backend/docker-compose.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ app:
3
+ build:
4
+ dockerfile: backend/Dockerfile
5
+ context: ..
6
+ image: jhj0517/whisper-webui-backend:latest
7
+
8
+ volumes:
9
+ # You can mount the container's volume paths to directory paths on your local machine.
10
+ # Models will be stored in the `./models' directory on your machine.
11
+ # Similarly, all output files will be stored in the `./outputs` directory.
12
+ # The DB file is saved in /Whisper-WebUI/backend/records.db unless you edit it in /Whisper-WebUI/backend/configs/.env
13
+ - ./models:/Whisper-WebUI/models
14
+ - ./outputs:/Whisper-WebUI/outputs
15
+ - ./backend:/Whisper-WebUI/backend
16
+
17
+ ports:
18
+ - "8000:8000"
19
+
20
+ stdin_open: true
21
+ tty: true
22
+
23
+ entrypoint: ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
24
+
25
+ # If you're not using Nvidia GPU, Update device to match yours.
26
+ # See more info at : https://docs.docker.com/compose/compose-file/deploy/#driver
27
+ deploy:
28
+ resources:
29
+ reservations:
30
+ devices:
31
+ - driver: nvidia
32
+ count: all
33
+ capabilities: [ gpu ]
backend/main.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import (
3
+ FastAPI,
4
+ )
5
+ from fastapi.responses import RedirectResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ import os
8
+ import time
9
+ import threading
10
+
11
+ from backend.db.db_instance import init_db
12
+ from backend.routers.transcription.router import transcription_router, get_pipeline
13
+ from backend.routers.vad.router import get_vad_model, vad_router
14
+ from backend.routers.bgm_separation.router import get_bgm_separation_inferencer, bgm_separation_router
15
+ from backend.routers.task.router import task_router
16
+ from backend.common.config_loader import read_env, load_server_config
17
+ from backend.common.cache_manager import cleanup_old_files
18
+ from modules.utils.paths import SERVER_CONFIG_PATH, BACKEND_CACHE_DIR
19
+
20
+
21
+ def clean_cache_thread(ttl: int, frequency: int) -> threading.Thread:
22
+ def clean_cache(_ttl: int, _frequency: int):
23
+ while True:
24
+ cleanup_old_files(cache_dir=BACKEND_CACHE_DIR, ttl=_ttl)
25
+ time.sleep(_frequency)
26
+
27
+ return threading.Thread(
28
+ target=clean_cache,
29
+ args=(ttl, frequency),
30
+ daemon=True
31
+ )
32
+
33
+
34
+ @asynccontextmanager
35
+ async def lifespan(app: FastAPI):
36
+ # Basic setup initialization
37
+ server_config = load_server_config()
38
+ read_env("DB_URL") # Place .env file into /configs/.env
39
+ init_db()
40
+
41
+ # Inferencer initialization
42
+ transcription_pipeline = get_pipeline()
43
+ vad_inferencer = get_vad_model()
44
+ bgm_separation_inferencer = get_bgm_separation_inferencer()
45
+
46
+ # Thread initialization
47
+ cache_thread = clean_cache_thread(server_config["cache"]["ttl"], server_config["cache"]["frequency"])
48
+ cache_thread.start()
49
+
50
+ yield
51
+
52
+ # Release VRAM when server shutdown
53
+ transcription_pipeline = None
54
+ vad_inferencer = None
55
+ bgm_separation_inferencer = None
56
+
57
+
58
+ app = FastAPI(
59
+ title="Whisper-WebUI-Backend",
60
+ description=f"""
61
+ REST API for Whisper-WebUI. Swagger UI is available via /docs or root URL with redirection. Redoc is available via /redoc.
62
+ """,
63
+ version="0.0.1",
64
+ lifespan=lifespan,
65
+ openapi_tags=[
66
+ {
67
+ "name": "BGM Separation",
68
+ "description": "Cached files for /bgm-separation are generated in the `backend/cache` directory,"
69
+ " you can set TLL for these files in `backend/configs/config.yaml`."
70
+ }
71
+ ]
72
+ )
73
+ app.add_middleware(
74
+ CORSMiddleware,
75
+ allow_origins=["*"],
76
+ allow_credentials=True,
77
+ allow_methods=["GET", "POST", "PUT", "PATCH", "OPTIONS"], # Disable DELETE
78
+ allow_headers=["*"],
79
+ )
80
+ app.include_router(transcription_router)
81
+ app.include_router(vad_router)
82
+ app.include_router(bgm_separation_router)
83
+ app.include_router(task_router)
84
+
85
+
86
+ @app.get("/", response_class=RedirectResponse, include_in_schema=False)
87
+ async def index():
88
+ """
89
+ Redirect to the documentation. Defaults to Swagger UI.
90
+ You can also check the /redoc with redoc style: https://github.com/Redocly/redoc
91
+ """
92
+ return "/docs"
backend/nginx/logs/logs_are_generated_here ADDED
File without changes
backend/nginx/nginx.conf ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes 1;
2
+
3
+ events {
4
+ worker_connections 1024;
5
+ }
6
+
7
+ http {
8
+ server {
9
+ listen 80;
10
+ client_max_body_size 4G;
11
+
12
+ server_name your-own-domain-name.com;
13
+
14
+ location / {
15
+ proxy_pass http://127.0.0.1:8000;
16
+ proxy_set_header Host $host;
17
+ proxy_set_header X-Real-IP $remote_addr;
18
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
19
+ proxy_set_header X-Forwarded-Proto $scheme;
20
+ }
21
+ }
22
+ }
23
+
backend/nginx/temp/temps_are_generated_here ADDED
File without changes
backend/requirements-backend.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Whisper-WebUI dependencies
2
+ -r ../requirements.txt
3
+
4
+ # Backend dependencies
5
+ python-dotenv
6
+ uvicorn
7
+ SQLAlchemy
8
+ sqlmodel
9
+ pydantic
10
+
11
+ # Test dependencies
12
+ # pytest
13
+ # pytest-asyncio
backend/routers/__init__.py ADDED
File without changes
backend/routers/bgm_separation/__init__.py ADDED
File without changes
backend/routers/bgm_separation/models.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class BGMSeparationResult(BaseModel):
5
+ instrumental_hash: str = Field(..., description="Instrumental file hash")
6
+ vocal_hash: str = Field(..., description="Vocal file hash")
backend/routers/bgm_separation/router.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import numpy as np
3
+ from fastapi import (
4
+ File,
5
+ UploadFile,
6
+ )
7
+ import gradio as gr
8
+ from fastapi import APIRouter, BackgroundTasks, Depends, Response, status
9
+ from fastapi.responses import FileResponse
10
+ from typing import List, Dict, Tuple
11
+ from datetime import datetime
12
+ import os
13
+
14
+ from modules.whisper.data_classes import *
15
+ from modules.uvr.music_separator import MusicSeparator
16
+ from modules.utils.paths import BACKEND_CACHE_DIR
17
+ from backend.common.audio import read_audio
18
+ from backend.common.models import QueueResponse
19
+ from backend.common.config_loader import load_server_config
20
+ from backend.common.compresser import get_file_hash, find_file_by_hash
21
+ from backend.db.task.models import TaskStatus, TaskType, ResultType
22
+ from backend.db.task.dao import add_task_to_db, update_task_status_in_db
23
+ from .models import BGMSeparationResult
24
+
25
+
26
+ bgm_separation_router = APIRouter(prefix="/bgm-separation", tags=["BGM Separation"])
27
+
28
+
29
+ @functools.lru_cache
30
+ def get_bgm_separation_inferencer() -> 'MusicSeparator':
31
+ config = load_server_config()["bgm_separation"]
32
+ inferencer = MusicSeparator(
33
+ output_dir=os.path.join(BACKEND_CACHE_DIR, "UVR")
34
+ )
35
+ inferencer.update_model(
36
+ model_name=config["model_size"],
37
+ device=config["device"]
38
+ )
39
+ return inferencer
40
+
41
+
42
+ def run_bgm_separation(
43
+ audio: np.ndarray,
44
+ params: BGMSeparationParams,
45
+ identifier: str,
46
+ ) -> Tuple[np.ndarray, np.ndarray]:
47
+ update_task_status_in_db(
48
+ identifier=identifier,
49
+ update_data={
50
+ "uuid": identifier,
51
+ "status": TaskStatus.IN_PROGRESS,
52
+ "updated_at": datetime.utcnow()
53
+ }
54
+ )
55
+
56
+ start_time = datetime.utcnow()
57
+ instrumental, vocal, filepaths = get_bgm_separation_inferencer().separate(
58
+ audio=audio,
59
+ model_name=params.uvr_model_size,
60
+ device=params.uvr_device,
61
+ segment_size=params.segment_size,
62
+ save_file=True,
63
+ progress=gr.Progress()
64
+ )
65
+ instrumental_path, vocal_path = filepaths
66
+ elapsed_time = (datetime.utcnow() - start_time).total_seconds()
67
+
68
+ update_task_status_in_db(
69
+ identifier=identifier,
70
+ update_data={
71
+ "uuid": identifier,
72
+ "status": TaskStatus.COMPLETED,
73
+ "result": BGMSeparationResult(
74
+ instrumental_hash=get_file_hash(instrumental_path),
75
+ vocal_hash=get_file_hash(vocal_path)
76
+ ).model_dump(),
77
+ "result_type": ResultType.FILEPATH,
78
+ "updated_at": datetime.utcnow(),
79
+ "duration": elapsed_time
80
+ }
81
+ )
82
+ return instrumental, vocal
83
+
84
+
85
+ @bgm_separation_router.post(
86
+ "/",
87
+ response_model=QueueResponse,
88
+ status_code=status.HTTP_201_CREATED,
89
+ summary="Separate Background BGM abd vocal",
90
+ description="Separate background music and vocal from an uploaded audio or video file.",
91
+ )
92
+ async def bgm_separation(
93
+ background_tasks: BackgroundTasks,
94
+ file: UploadFile = File(..., description="Audio or video file to separate background music."),
95
+ params: BGMSeparationParams = Depends()
96
+ ) -> QueueResponse:
97
+ if not isinstance(file, np.ndarray):
98
+ audio, info = await read_audio(file=file)
99
+ else:
100
+ audio, info = file, None
101
+
102
+ identifier = add_task_to_db(
103
+ status=TaskStatus.QUEUED,
104
+ file_name=file.filename,
105
+ audio_duration=info.duration if info else None,
106
+ task_type=TaskType.BGM_SEPARATION,
107
+ task_params=params.model_dump(),
108
+ )
109
+
110
+ background_tasks.add_task(
111
+ run_bgm_separation,
112
+ audio=audio,
113
+ params=params,
114
+ identifier=identifier
115
+ )
116
+
117
+ return QueueResponse(identifier=identifier, status=TaskStatus.QUEUED, message="BGM Separation task has queued")
118
+
119
+
backend/routers/task/__init__.py ADDED
File without changes
backend/routers/task/router.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from fastapi.responses import FileResponse
3
+ from sqlalchemy.orm import Session
4
+ import os
5
+
6
+ from backend.db.db_instance import get_db_session
7
+ from backend.db.task.dao import (
8
+ get_task_status_from_db,
9
+ get_all_tasks_status_from_db,
10
+ delete_task_from_db,
11
+ )
12
+ from backend.db.task.models import (
13
+ TasksResult,
14
+ Task,
15
+ TaskStatusResponse,
16
+ TaskType
17
+ )
18
+ from backend.common.models import (
19
+ Response,
20
+ )
21
+ from backend.common.compresser import compress_files, find_file_by_hash
22
+ from modules.utils.paths import BACKEND_CACHE_DIR
23
+
24
+ task_router = APIRouter(prefix="/task", tags=["Tasks"])
25
+
26
+
27
+ @task_router.get(
28
+ "/{identifier}",
29
+ response_model=TaskStatusResponse,
30
+ status_code=status.HTTP_200_OK,
31
+ summary="Retrieve Task by Identifier",
32
+ description="Retrieve the specific task by its identifier.",
33
+ )
34
+ async def get_task(
35
+ identifier: str,
36
+ session: Session = Depends(get_db_session),
37
+ ) -> TaskStatusResponse:
38
+ """
39
+ Retrieve the specific task by its identifier.
40
+ """
41
+ task = get_task_status_from_db(identifier=identifier, session=session)
42
+
43
+ if task is not None:
44
+ return task.to_response()
45
+ else:
46
+ raise HTTPException(status_code=404, detail="Identifier not found")
47
+
48
+
49
+ @task_router.get(
50
+ "/file/{identifier}",
51
+ status_code=status.HTTP_200_OK,
52
+ summary="Retrieve FileResponse Task by Identifier",
53
+ description="Retrieve the file response task by its identifier. You can use this endpoint if you need to download"
54
+ " The file as a response",
55
+ )
56
+ async def get_file_task(
57
+ identifier: str,
58
+ session: Session = Depends(get_db_session),
59
+ ) -> FileResponse:
60
+ """
61
+ Retrieve the downloadable file response of a specific task by its identifier.
62
+ Compressed by ZIP basically.
63
+ """
64
+ task = get_task_status_from_db(identifier=identifier, session=session)
65
+
66
+ if task is not None:
67
+ if task.task_type == TaskType.BGM_SEPARATION:
68
+ output_zip_path = os.path.join(BACKEND_CACHE_DIR, f"{identifier}_bgm_separation.zip")
69
+ instrumental_path = find_file_by_hash(
70
+ os.path.join(BACKEND_CACHE_DIR, "UVR", "instrumental"),
71
+ task.result["instrumental_hash"]
72
+ )
73
+ vocal_path = find_file_by_hash(
74
+ os.path.join(BACKEND_CACHE_DIR, "UVR", "vocals"),
75
+ task.result["vocal_hash"]
76
+ )
77
+
78
+ output_zip_path = compress_files(
79
+ [instrumental_path, vocal_path],
80
+ output_zip_path
81
+ )
82
+ return FileResponse(
83
+ path=output_zip_path,
84
+ status_code=200,
85
+ filename=output_zip_path,
86
+ media_type="application/zip"
87
+ )
88
+ else:
89
+ raise HTTPException(status_code=404, detail=f"File download is only supported for bgm separation."
90
+ f" The given type is {task.task_type}")
91
+ else:
92
+ raise HTTPException(status_code=404, detail="Identifier not found")
93
+
94
+
95
+ # Delete method, commented by default because this endpoint is likely to require special permissions
96
+ # @task_router.delete(
97
+ # "/{identifier}",
98
+ # response_model=Response,
99
+ # status_code=status.HTTP_200_OK,
100
+ # summary="Delete Task by Identifier",
101
+ # description="Delete a task from the system using its identifier.",
102
+ # )
103
+ async def delete_task(
104
+ identifier: str,
105
+ session: Session = Depends(get_db_session),
106
+ ) -> Response:
107
+ """
108
+ Delete a task by its identifier.
109
+ """
110
+ if delete_task_from_db(identifier, session):
111
+ return Response(identifier=identifier, message="Task deleted")
112
+ else:
113
+ raise HTTPException(status_code=404, detail="Task not found")
114
+
115
+
116
+ # Get All method, commented by default because this endpoint is likely to require special permissions
117
+ # @task_router.get(
118
+ # "/all",
119
+ # response_model=TasksResult,
120
+ # status_code=status.HTTP_200_OK,
121
+ # summary="Retrieve All Task Statuses",
122
+ # description="Retrieve the statuses of all tasks available in the system.",
123
+ # )
124
+ async def get_all_tasks_status(
125
+ session: Session = Depends(get_db_session),
126
+ ) -> TasksResult:
127
+ """
128
+ Retrieve all tasks.
129
+ """
130
+ return get_all_tasks_status_from_db(session=session)
backend/routers/transcription/__init__.py ADDED
File without changes
backend/routers/transcription/router.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import uuid
3
+ import numpy as np
4
+ from fastapi import (
5
+ File,
6
+ UploadFile,
7
+ )
8
+ import gradio as gr
9
+ from fastapi import APIRouter, BackgroundTasks, Depends, Response, status
10
+ from typing import List, Dict
11
+ from sqlalchemy.orm import Session
12
+ from datetime import datetime
13
+ from modules.whisper.data_classes import *
14
+ from modules.utils.paths import BACKEND_CACHE_DIR
15
+ from modules.whisper.faster_whisper_inference import FasterWhisperInference
16
+ from backend.common.audio import read_audio
17
+ from backend.common.models import QueueResponse
18
+ from backend.common.config_loader import load_server_config
19
+ from backend.db.task.dao import (
20
+ add_task_to_db,
21
+ get_db_session,
22
+ update_task_status_in_db
23
+ )
24
+ from backend.db.task.models import TaskStatus, TaskType
25
+
26
+ transcription_router = APIRouter(prefix="/transcription", tags=["Transcription"])
27
+
28
+
29
+ @functools.lru_cache
30
+ def get_pipeline() -> 'FasterWhisperInference':
31
+ config = load_server_config()["whisper"]
32
+ inferencer = FasterWhisperInference(
33
+ output_dir=BACKEND_CACHE_DIR
34
+ )
35
+ inferencer.update_model(
36
+ model_size=config["model_size"],
37
+ compute_type=config["compute_type"]
38
+ )
39
+ return inferencer
40
+
41
+
42
+ def run_transcription(
43
+ audio: np.ndarray,
44
+ params: TranscriptionPipelineParams,
45
+ identifier: str,
46
+ ) -> List[Segment]:
47
+ update_task_status_in_db(
48
+ identifier=identifier,
49
+ update_data={
50
+ "uuid": identifier,
51
+ "status": TaskStatus.IN_PROGRESS,
52
+ "updated_at": datetime.utcnow()
53
+ },
54
+ )
55
+
56
+ segments, elapsed_time = get_pipeline().run(
57
+ audio,
58
+ gr.Progress(),
59
+ "SRT",
60
+ False,
61
+ *params.to_list()
62
+ )
63
+ segments = [seg.model_dump() for seg in segments]
64
+
65
+ update_task_status_in_db(
66
+ identifier=identifier,
67
+ update_data={
68
+ "uuid": identifier,
69
+ "status": TaskStatus.COMPLETED,
70
+ "result": segments,
71
+ "updated_at": datetime.utcnow(),
72
+ "duration": elapsed_time
73
+ },
74
+ )
75
+ return segments
76
+
77
+
78
+ @transcription_router.post(
79
+ "/",
80
+ response_model=QueueResponse,
81
+ status_code=status.HTTP_201_CREATED,
82
+ summary="Transcribe Audio",
83
+ description="Process the provided audio or video file to generate a transcription.",
84
+ )
85
+ async def transcription(
86
+ background_tasks: BackgroundTasks,
87
+ file: UploadFile = File(..., description="Audio or video file to transcribe."),
88
+ whisper_params: WhisperParams = Depends(),
89
+ vad_params: VadParams = Depends(),
90
+ bgm_separation_params: BGMSeparationParams = Depends(),
91
+ diarization_params: DiarizationParams = Depends(),
92
+ ) -> QueueResponse:
93
+ if not isinstance(file, np.ndarray):
94
+ audio, info = await read_audio(file=file)
95
+ else:
96
+ audio, info = file, None
97
+
98
+ params = TranscriptionPipelineParams(
99
+ whisper=whisper_params,
100
+ vad=vad_params,
101
+ bgm_separation=bgm_separation_params,
102
+ diarization=diarization_params
103
+ )
104
+
105
+ identifier = add_task_to_db(
106
+ status=TaskStatus.QUEUED,
107
+ file_name=file.filename,
108
+ audio_duration=info.duration if info else None,
109
+ language=params.whisper.lang,
110
+ task_type=TaskType.TRANSCRIPTION,
111
+ task_params=params.to_dict(),
112
+ )
113
+
114
+ background_tasks.add_task(
115
+ run_transcription,
116
+ audio=audio,
117
+ params=params,
118
+ identifier=identifier,
119
+ )
120
+
121
+ return QueueResponse(identifier=identifier, status=TaskStatus.QUEUED, message="Transcription task has queued")
122
+
123
+
backend/routers/vad/__init__.py ADDED
File without changes
backend/routers/vad/router.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import numpy as np
3
+ from faster_whisper.vad import VadOptions
4
+ from fastapi import (
5
+ File,
6
+ UploadFile,
7
+ )
8
+ from fastapi import APIRouter, BackgroundTasks, Depends, Response, status
9
+ from typing import List, Dict
10
+ from datetime import datetime
11
+
12
+ from modules.vad.silero_vad import SileroVAD
13
+ from modules.whisper.data_classes import VadParams
14
+ from backend.common.audio import read_audio
15
+ from backend.common.models import QueueResponse
16
+ from backend.db.task.dao import add_task_to_db, update_task_status_in_db
17
+ from backend.db.task.models import TaskStatus, TaskType
18
+
19
+ vad_router = APIRouter(prefix="/vad", tags=["Voice Activity Detection"])
20
+
21
+
22
+ @functools.lru_cache
23
+ def get_vad_model() -> SileroVAD:
24
+ inferencer = SileroVAD()
25
+ inferencer.update_model()
26
+ return inferencer
27
+
28
+
29
+ def run_vad(
30
+ audio: np.ndarray,
31
+ params: VadOptions,
32
+ identifier: str,
33
+ ) -> List[Dict]:
34
+ update_task_status_in_db(
35
+ identifier=identifier,
36
+ update_data={
37
+ "uuid": identifier,
38
+ "status": TaskStatus.IN_PROGRESS,
39
+ "updated_at": datetime.utcnow()
40
+ }
41
+ )
42
+
43
+ start_time = datetime.utcnow()
44
+ audio, speech_chunks = get_vad_model().run(
45
+ audio=audio,
46
+ vad_parameters=params
47
+ )
48
+ elapsed_time = (datetime.utcnow() - start_time).total_seconds()
49
+
50
+ update_task_status_in_db(
51
+ identifier=identifier,
52
+ update_data={
53
+ "uuid": identifier,
54
+ "status": TaskStatus.COMPLETED,
55
+ "updated_at": datetime.utcnow(),
56
+ "result": speech_chunks,
57
+ "duration": elapsed_time
58
+ }
59
+ )
60
+
61
+ return speech_chunks
62
+
63
+
64
+ @vad_router.post(
65
+ "/",
66
+ response_model=QueueResponse,
67
+ status_code=status.HTTP_201_CREATED,
68
+ summary="Voice Activity Detection",
69
+ description="Detect voice parts in the provided audio or video file to generate a timeline of speech segments.",
70
+ )
71
+ async def vad(
72
+ background_tasks: BackgroundTasks,
73
+ file: UploadFile = File(..., description="Audio or video file to detect voices."),
74
+ params: VadParams = Depends()
75
+ ) -> QueueResponse:
76
+ if not isinstance(file, np.ndarray):
77
+ audio, info = await read_audio(file=file)
78
+ else:
79
+ audio, info = file, None
80
+
81
+ vad_options = VadOptions(
82
+ threshold=params.threshold,
83
+ min_speech_duration_ms=params.min_speech_duration_ms,
84
+ max_speech_duration_s=params.max_speech_duration_s,
85
+ min_silence_duration_ms=params.min_silence_duration_ms,
86
+ speech_pad_ms=params.speech_pad_ms
87
+ )
88
+
89
+ identifier = add_task_to_db(
90
+ status=TaskStatus.QUEUED,
91
+ file_name=file.filename,
92
+ audio_duration=info.duration if info else None,
93
+ task_type=TaskType.VAD,
94
+ task_params=params.model_dump(),
95
+ )
96
+
97
+ background_tasks.add_task(run_vad, audio=audio, params=vad_options, identifier=identifier)
98
+
99
+ return QueueResponse(identifier=identifier, status=TaskStatus.QUEUED, message="VAD task has queued")
100
+
101
+
backend/tests/__init__.py ADDED
File without changes
backend/tests/test_backend_bgm_separation.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi import UploadFile
3
+ from io import BytesIO
4
+ import os
5
+ import torch
6
+
7
+ from backend.db.task.models import TaskStatus
8
+ from backend.tests.test_task_status import wait_for_task_completion, fetch_file_response
9
+ from backend.tests.test_backend_config import (
10
+ get_client, setup_test_file, get_upload_file_instance, calculate_wer,
11
+ TEST_BGM_SEPARATION_PARAMS, TEST_ANSWER, TEST_BGM_SEPARATION_OUTPUT_PATH
12
+ )
13
+
14
+
15
+ @pytest.mark.skipif(not torch.cuda.is_available(), reason="Skip the test because CUDA is not available")
16
+ @pytest.mark.parametrize(
17
+ "bgm_separation_params",
18
+ [
19
+ TEST_BGM_SEPARATION_PARAMS
20
+ ]
21
+ )
22
+ def test_transcription_endpoint(
23
+ get_upload_file_instance,
24
+ bgm_separation_params: dict
25
+ ):
26
+ client = get_client()
27
+ file_content = BytesIO(get_upload_file_instance.file.read())
28
+ get_upload_file_instance.file.seek(0)
29
+
30
+ response = client.post(
31
+ "/bgm-separation",
32
+ files={"file": (get_upload_file_instance.filename, file_content, "audio/mpeg")},
33
+ params=bgm_separation_params
34
+ )
35
+
36
+ assert response.status_code == 201
37
+ assert response.json()["status"] == TaskStatus.QUEUED
38
+ task_identifier = response.json()["identifier"]
39
+ assert isinstance(task_identifier, str) and task_identifier
40
+
41
+ completed_task = wait_for_task_completion(
42
+ identifier=task_identifier
43
+ )
44
+
45
+ assert completed_task is not None, f"Task with identifier {task_identifier} did not complete within the " \
46
+ f"expected time."
47
+
48
+ result = completed_task.json()["result"]
49
+ assert "instrumental_hash" in result and result["instrumental_hash"]
50
+ assert "vocal_hash" in result and result["vocal_hash"]
51
+
52
+ file_response = fetch_file_response(task_identifier)
53
+ assert file_response.status_code == 200, f"Fetching File Response has failed. Response is: {file_response}"
54
+
55
+ with open(TEST_BGM_SEPARATION_OUTPUT_PATH, "wb") as file:
56
+ file.write(file_response.content)
57
+
58
+ assert os.path.exists(TEST_BGM_SEPARATION_OUTPUT_PATH)
59
+
backend/tests/test_backend_config.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ from fastapi import FastAPI, UploadFile
3
+ from fastapi.testclient import TestClient
4
+ from starlette.datastructures import UploadFile as StarletteUploadFile
5
+ from io import BytesIO
6
+ import os
7
+ import requests
8
+ import pytest
9
+ import yaml
10
+ import jiwer
11
+
12
+ from backend.main import app
13
+ from modules.whisper.data_classes import *
14
+ from modules.utils.paths import *
15
+ from modules.utils.files_manager import load_yaml, save_yaml
16
+
17
+ TEST_PIPELINE_PARAMS = {**WhisperParams(model_size="tiny", compute_type="float32").model_dump(exclude_none=True),
18
+ **VadParams().model_dump(exclude_none=True),
19
+ **BGMSeparationParams().model_dump(exclude_none=True),
20
+ **DiarizationParams().model_dump(exclude_none=True)}
21
+ TEST_VAD_PARAMS = VadParams().model_dump()
22
+ TEST_BGM_SEPARATION_PARAMS = BGMSeparationParams().model_dump()
23
+ TEST_FILE_DOWNLOAD_URL = "https://github.com/jhj0517/whisper_flutter_new/raw/main/example/assets/jfk.wav"
24
+ TEST_FILE_PATH = os.path.join(WEBUI_DIR, "backend", "tests", "jfk.wav")
25
+ TEST_BGM_SEPARATION_OUTPUT_PATH = os.path.join(WEBUI_DIR, "backend", "tests", "separated_audio.zip")
26
+ TEST_ANSWER = "And so my fellow Americans ask not what your country can do for you ask what you can do for your country"
27
+ TEST_WHISPER_MODEL = "tiny"
28
+ TEST_COMPUTE_TYPE = "float32"
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def setup_test_file():
33
+ @functools.lru_cache
34
+ def download_file(url=TEST_FILE_DOWNLOAD_URL, file_path=TEST_FILE_PATH):
35
+ if os.path.exists(file_path):
36
+ return
37
+
38
+ if not os.path.exists(os.path.dirname(file_path)):
39
+ os.makedirs(os.path.dirname(file_path))
40
+
41
+ response = requests.get(url)
42
+
43
+ with open(file_path, "wb") as file:
44
+ file.write(response.content)
45
+
46
+ print(f"File downloaded to: {file_path}")
47
+
48
+ download_file(TEST_FILE_DOWNLOAD_URL, TEST_FILE_PATH)
49
+
50
+
51
+ @pytest.fixture
52
+ @functools.lru_cache
53
+ def get_upload_file_instance(filepath: str = TEST_FILE_PATH) -> UploadFile:
54
+ with open(filepath, "rb") as f:
55
+ file_contents = BytesIO(f.read())
56
+ filename = os.path.basename(filepath)
57
+ upload_file = StarletteUploadFile(file=file_contents, filename=filename)
58
+ return upload_file
59
+
60
+
61
+ @functools.lru_cache
62
+ def get_client(app: FastAPI = app):
63
+ return TestClient(app)
64
+
65
+
66
+ def calculate_wer(answer, prediction):
67
+ return jiwer.wer(answer, prediction)
backend/tests/test_backend_transcription.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi import UploadFile
3
+ from io import BytesIO
4
+
5
+ from backend.db.task.models import TaskStatus
6
+ from backend.tests.test_task_status import wait_for_task_completion
7
+ from backend.tests.test_backend_config import (
8
+ get_client, setup_test_file, get_upload_file_instance, calculate_wer,
9
+ TEST_PIPELINE_PARAMS, TEST_ANSWER
10
+ )
11
+
12
+
13
+ @pytest.mark.parametrize(
14
+ "pipeline_params",
15
+ [
16
+ TEST_PIPELINE_PARAMS
17
+ ]
18
+ )
19
+ def test_transcription_endpoint(
20
+ get_upload_file_instance,
21
+ pipeline_params: dict
22
+ ):
23
+ client = get_client()
24
+ file_content = BytesIO(get_upload_file_instance.file.read())
25
+ get_upload_file_instance.file.seek(0)
26
+
27
+ response = client.post(
28
+ "/transcription",
29
+ files={"file": (get_upload_file_instance.filename, file_content, "audio/mpeg")},
30
+ params=pipeline_params
31
+ )
32
+
33
+ assert response.status_code == 201
34
+ assert response.json()["status"] == TaskStatus.QUEUED
35
+ task_identifier = response.json()["identifier"]
36
+ assert isinstance(task_identifier, str) and task_identifier
37
+
38
+ completed_task = wait_for_task_completion(
39
+ identifier=task_identifier
40
+ )
41
+
42
+ assert completed_task is not None, f"Task with identifier {task_identifier} did not complete within the " \
43
+ f"expected time."
44
+
45
+ result = completed_task.json()["result"]
46
+ assert result, "Transcription text is empty"
47
+
48
+ wer = calculate_wer(TEST_ANSWER, result[0]["text"].strip().replace(",", "").replace(".", ""))
49
+ assert wer < 0.1, f"WER is too high, it's {wer}"
50
+