Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 308ac1cff7 | |||
| 2fced08705 | |||
| 8bdaf9249d | |||
| b47a56ca6d | |||
| f975bd452e | |||
| cb963c4834 | |||
| 1db94ea96e | |||
| babe5de074 | |||
| 99af50208d | |||
| dc22b7da9f | |||
| 2e9f67ba0b | |||
| c919ba3501 | |||
| a38fdb494d | |||
| fddc244228 | |||
| 5e1174ff33 | |||
| e40414ab1b | |||
| 17873c66a0 | |||
| 1147f58225 | |||
| 0baa1dc0a6 | |||
| d1de4948ee | |||
| ff871ad485 | |||
| 5fe5e0c8ba | |||
| b42ced9816 | |||
| 06794470f8 | |||
| d530957b2c | |||
| 6cabbe441b | |||
| 78da3f6750 | |||
| b04cffc458 | |||
| fd7c5965b3 | |||
| 4471665085 | |||
| 147e97002e | |||
| e3c7666cf7 | |||
| 8266099ed0 | |||
| 01dc69e068 | |||
| 9bb92b9bb2 | |||
| 57c4b60e04 | |||
| 3cd96367fb | |||
| c1420cba0d | |||
| 4db91eed66 | |||
| 7bcb92c266 | |||
| 170ba22e5b | |||
| ac00e28b86 | |||
| ceb3cc8747 | |||
| eaec0ead08 | |||
| 9fbff47126 | |||
| b4abe95fc6 | |||
| 14974af951 | |||
| bc474b4a76 | |||
| 9ccf940f51 | |||
| 9a9972007e | |||
| b2ad6478f5 | |||
| 490efdeacc | |||
| cb570d28ce | |||
| 4e5e086c38 | |||
| cf78d5d608 | |||
| 9d29b08cea | |||
| 6071cc1cc5 | |||
| f98e309663 | |||
| 30b00d6c89 | |||
| da2992bcaf | |||
| 16c5ed8ce9 | |||
| e14fefb671 | |||
| 98399707a3 | |||
| 567ceb1246 | |||
| 28ea8a20f1 | |||
| acf6dfe5b7 | |||
| 84a97f5fdd | |||
| ca2634bbb6 | |||
| 444ce63440 | |||
| 8db063ee33 | |||
| 92cbc37e9c | |||
| 24fd835356 | |||
| 5409d14bcb | |||
| d6edf8e847 | |||
| cc3ed74c0e | |||
| 20a8a8ad3d | |||
| 07387abbc0 | |||
| 4ecc59783e | |||
| ec9074d712 | |||
| 3a25db4cb9 | |||
| 4924ec0adb | |||
| f35abc7f81 | |||
| f383121ec3 | |||
| 17d62272cf | |||
| 91e1b75bfc | |||
| 7aad2ae721 | |||
| 6a1b82f953 | |||
| dc84839873 | |||
| 56d19f5469 | |||
| 08fa183ba4 | |||
| d89b27b8aa | |||
| ce68cc6c87 | |||
| e697574870 | |||
| b098b52a4d | |||
| ad5543b03e | |||
| 60455b1583 | |||
| cb458fc207 | |||
| 32ed089a76 | |||
| 5b28ddefbd | |||
| e1f531eccf | |||
| 8200207530 | |||
| 08575a03c2 | |||
| f590446865 | |||
| 36d137888e | |||
| e64bc9f3d6 | |||
| 7cc945aded | |||
| 2c8a25d355 | |||
| f4027de343 | |||
| d1754d2c46 | |||
| 0e6b1c0632 | |||
| d6b51ccd7d | |||
| 2f3c1cd172 | |||
| 703263b375 | |||
| 30d2cffb93 | |||
| 4d94c6b38b | |||
| d5a0f5859e | |||
| 025873d2ca | |||
| 8c36768f7f | |||
| ce13e7b622 | |||
| 3498787ccd | |||
| 5cd59b1e4c | |||
| bd543295f3 | |||
| 8e2642283a | |||
| 3bf5b47947 | |||
| 634dae835b | |||
| 969a5aa9e5 | |||
| 44a2e20c68 | |||
| 986823dbef | |||
| 1e2faa3f2b | |||
| e3084b34cb | |||
| b955e63dc1 | |||
| f25ff1785a | |||
| 867ff522ae | |||
| 75001ae6b7 | |||
| 6f1d13f25b | |||
| 7a9dc6db40 | |||
| 735d6c7763 | |||
| 0942dc2cfd | |||
| 881fd55776 | |||
| 0c01d7b1e5 | |||
| c810369324 | |||
| 71d0fe69c6 | |||
| 67232fffd5 | |||
| 076aebf3b6 | |||
| 4cf9d95f73 | |||
| 389bb5ae37 | |||
| a7eedc5d84 | |||
| d91330d790 | |||
| 783d147316 | |||
| 058c93e55e | |||
| f06b9bc827 | |||
| 3c202bf836 | |||
| 647c576e6a | |||
| 71a062b726 | |||
| a26f990586 | |||
| ddb1e0947f | |||
| 6dff4fbdd3 | |||
| 244ca9e6ba | |||
| 0f9e93d203 | |||
| fd86340f30 | |||
| 2300eedc8b | |||
| cafcb04fbc | |||
| 72ead71eeb | |||
| 7b2f5cff72 | |||
| 32c6a565d7 | |||
| e30286c046 | |||
| 7c0b32b85e | |||
| 01665a54c1 | |||
| 02793a93f8 | |||
| db2e0bbcdd | |||
| e92ddd291a | |||
| 5918b5ed42 | |||
| 71d207a607 | |||
| 6ee4cd09f2 | |||
| 5de4de4b84 | |||
| e006722da7 | |||
| a52dc0cbf8 | |||
| 048ab0a8f4 | |||
| 261bb9e961 | |||
| 091f6179d4 | |||
| 1e1349cd80 | |||
| 14beb4f942 | |||
| 7ffcad64ba | |||
| 402fceb9f3 | |||
| 09b18e8ab8 | |||
| da72d03073 | |||
| a1a8d5f92a | |||
| b6dee4e46e | |||
| f3cd20fbf3 | |||
| da86c18205 | |||
| 8097e9b44a | |||
| 222852ff33 | |||
| 2de67ee02f | |||
| 073cfc20f3 | |||
| ee80bd21bd | |||
| 410b91d133 | |||
| a2b5220738 | |||
| 1938dfb490 |
+152
-36
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Test & Build CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,46 +7,162 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [ main ]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build-and-push-package:
|
||||
runs-on: ubuntu-latest
|
||||
run-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8, 3.9, '3.10', 3.11]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
!~/.cache/pip/log
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-${{ matrix.python-version }}-
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y ffmpeg portaudio19-dev
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements/server.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
||||
pip install -r requirements/client.txt
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "Running tests with Python ${{ matrix.python-version }}"
|
||||
python -m unittest discover -s tests
|
||||
|
||||
check-code-format:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8, 3.9, '3.10', 3.11]
|
||||
|
||||
steps:
|
||||
- name: Check Out Repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Set up FFmpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v2
|
||||
|
||||
- name: Install Additional requirements
|
||||
run: |
|
||||
sudo apt-get -y install portaudio19-dev wget
|
||||
shell: bash
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Client Requirements
|
||||
run: pip install -r requirements/client.txt
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8
|
||||
|
||||
- name: Install Server Requirements
|
||||
run: pip install -r requirements/server.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
- name: Install Wheel for build
|
||||
run: pip install wheel twine
|
||||
|
||||
- name: Build wheel
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Push package on Test PyPI
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
build-and-push-docker-cpu:
|
||||
needs: [run-tests, check-code-format]
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.cpu
|
||||
push: true
|
||||
tags: ghcr.io/collabora/whisperlive-cpu:latest
|
||||
|
||||
build-and-push-docker-gpu:
|
||||
needs: [run-tests, check-code-format, build-and-push-docker-cpu]
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Docker Prune
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push Docker GPU image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.gpu
|
||||
push: true
|
||||
tags: ghcr.io/collabora/whisperlive-gpu:latest
|
||||
|
||||
publish-to-pypi:
|
||||
needs: [run-tests, check-code-format]
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
!~/.cache/pip/log
|
||||
key: ubuntu-latest-pip-3.8-${{ hashFiles('requirements/server.txt', 'requirements/client.txt') }}
|
||||
restore-keys: |
|
||||
ubuntu-latest-pip-3.8-
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y ffmpeg portaudio19-dev
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install -r requirements/server.txt
|
||||
pip install -r requirements/client.txt
|
||||
|
||||
- name: Build package
|
||||
run: python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
@@ -26,9 +26,9 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
|
||||
### Options
|
||||
When using the Audio Transcription extension, you have the following options:
|
||||
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
||||
- **Use Multilingual Model**: Enable this option to utilize the multilingual capabilities of OpenAI-whisper.
|
||||
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
||||
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
||||
- **Model Size**: Select the whisper model size to run the server with.
|
||||
|
||||
### Getting Started
|
||||
- Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live).
|
||||
|
||||
@@ -156,7 +156,9 @@ async function startCapture(options) {
|
||||
port: options.port,
|
||||
multilingual: options.useMultilingual,
|
||||
language: options.language,
|
||||
task: options.task
|
||||
task: options.task,
|
||||
modelSize: options.modelSize,
|
||||
useVad: options.useVad,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -207,13 +209,3 @@ chrome.runtime.onMessage.addListener(async (message) => {
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Listens for if the tab is reloaded.
|
||||
* @param {Object} message - The message received from the runtime.
|
||||
*/
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === 'complete') {
|
||||
await executeScriptInTab(tabId, "content.js");
|
||||
await delayExecution(500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ function init_element() {
|
||||
|
||||
elem_container = document.createElement('div');
|
||||
elem_container.id = "transcription";
|
||||
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;top:0px;position:absolute;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
|
||||
elem_container.style.cssText = 'padding-top:16px;font-size:18px;position: fixed; top: 85%; left: 50%; transform: translate(-50%, -50%);line-height:18px;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
|
||||
|
||||
for (var i = 0; i < 4; i++) {
|
||||
elem_text = document.createElement('span');
|
||||
@@ -173,13 +173,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (type === "STOP") {
|
||||
remove_element();
|
||||
sendResponse({data: "STOPPED"});
|
||||
return;
|
||||
return true;
|
||||
} else if (type === "showWaitPopup"){
|
||||
initPopupElement();
|
||||
|
||||
showPopup(`Estimated wait time ~ ${Math.round(data)} minutes`);
|
||||
sendResponse({data: "popup"});
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
init_element();
|
||||
@@ -234,4 +234,5 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
}
|
||||
|
||||
sendResponse({});
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -93,16 +93,14 @@ async function startRecord(option) {
|
||||
const socket = new WebSocket(`ws://${option.host}:${option.port}/`);
|
||||
let isServerReady = false;
|
||||
let language = option.language;
|
||||
if (language === null && !option.multilingual) {
|
||||
language = 'en';
|
||||
}
|
||||
socket.onopen = function(e) {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
uid: uuid,
|
||||
multilingual: option.multilingual,
|
||||
language: option.language,
|
||||
task: option.task
|
||||
task: option.task,
|
||||
model: option.modelSize,
|
||||
use_vad: option.useVad
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -184,16 +182,17 @@ async function startRecord(option) {
|
||||
* @param {Object} sender - The sender object containing information about the message sender.
|
||||
* @param {Function} sendResponse - The function to send a response back to the message sender.
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
const { type, data } = request;
|
||||
|
||||
switch (type) {
|
||||
case "start_capture":
|
||||
await startRecord(data);
|
||||
startRecord(data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
sendResponse({});
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -16,120 +16,137 @@
|
||||
<label for="useServerCheckbox">Use Collabora Whisper-Live Server</label>
|
||||
</div>
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="useMultilingualCheckbox">
|
||||
<label for="useMultilingualCheckbox">Use Multilingual Model</label>
|
||||
<input type="checkbox" id="useVadCheckbox">
|
||||
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<label for="languageDropdown">Select Language:</label>
|
||||
<select id="languageDropdown" disabled>
|
||||
<option value="">Select Language</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="ta">Tamil</option>
|
||||
<option value="no">Norwegian</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="la">Latin</option>
|
||||
<option value="mi">Maori</option>
|
||||
<option value="ml">Malayalam</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="te">Telugu</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="az">Azerbaijani</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="kn">Kannada</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="mk">Macedonian</option>
|
||||
<option value="br">Breton</option>
|
||||
<option value="eu">Basque</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="hy">Armenian</option>
|
||||
<option value="ne">Nepali</option>
|
||||
<option value="mn">Mongolian</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="kk">Kazakh</option>
|
||||
<option value="sq">Albanian</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="gl">Galician</option>
|
||||
<option value="mr">Marathi</option>
|
||||
<option value="pa">Punjabi</option>
|
||||
<option value="si">Sinhala</option>
|
||||
<option value="km">Khmer</option>
|
||||
<option value="sn">Shona</option>
|
||||
<option value="yo">Yoruba</option>
|
||||
<option value="so">Somali</option>
|
||||
<select id="languageDropdown">
|
||||
<option value="" selected>Automatically detect</option>
|
||||
<option value="af">Afrikaans</option>
|
||||
<option value="oc">Occitan</option>
|
||||
<option value="ka">Georgian</option>
|
||||
<option value="be">Belarusian</option>
|
||||
<option value="tg">Tajik</option>
|
||||
<option value="sd">Sindhi</option>
|
||||
<option value="gu">Gujarati</option>
|
||||
<option value="sq">Albanian</option>
|
||||
<option value="am">Amharic</option>
|
||||
<option value="yi">Yiddish</option>
|
||||
<option value="lo">Lao</option>
|
||||
<option value="uz">Uzbek</option>
|
||||
<option value="fo">Faroese</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="ps">Pashto</option>
|
||||
<option value="tk">Turkmen</option>
|
||||
<option value="nn">Nynorsk</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="sa">Sanskrit</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="my">Myanmar</option>
|
||||
<option value="bo">Tibetan</option>
|
||||
<option value="tl">Tagalog</option>
|
||||
<option value="mg">Malagasy</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="hy">Armenian</option>
|
||||
<option value="as">Assamese</option>
|
||||
<option value="tt">Tatar</option>
|
||||
<option value="haw">Hawaiian</option>
|
||||
<option value="ln">Lingala</option>
|
||||
<option value="ha">Hausa</option>
|
||||
<option value="az">Azerbaijani</option>
|
||||
<option value="ba">Bashkir</option>
|
||||
<option value="eu">Basque</option>
|
||||
<option value="be">Belarusian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="br">Breton</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="fo">Faroese</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="gl">Galician</option>
|
||||
<option value="ka">Georgian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="gu">Gujarati</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="ha">Hausa</option>
|
||||
<option value="haw">Hawaiian</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="jw">Javanese</option>
|
||||
<option value="kn">Kannada</option>
|
||||
<option value="kk">Kazakh</option>
|
||||
<option value="km">Khmer</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="lo">Lao</option>
|
||||
<option value="la">Latin</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="ln">Lingala</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="mk">Macedonian</option>
|
||||
<option value="mg">Malagasy</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="ml">Malayalam</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="mi">Maori</option>
|
||||
<option value="mr">Marathi</option>
|
||||
<option value="mn">Mongolian</option>
|
||||
<option value="my">Myanmar</option>
|
||||
<option value="ne">Nepali</option>
|
||||
<option value="no">Norwegian</option>
|
||||
<option value="nn">Nynorsk</option>
|
||||
<option value="oc">Occitan</option>
|
||||
<option value="ps">Pashto</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="pa">Punjabi</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="sa">Sanskrit</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="sn">Shona</option>
|
||||
<option value="sd">Sindhi</option>
|
||||
<option value="si">Sinhala</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="so">Somali</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="su">Sundanese</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="tl">Tagalog</option>
|
||||
<option value="tg">Tajik</option>
|
||||
<option value="ta">Tamil</option>
|
||||
<option value="tt">Tatar</option>
|
||||
<option value="te">Telugu</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="bo">Tibetan</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="tk">Turkmen</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="uz">Uzbek</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="yi">Yiddish</option>
|
||||
<option value="yo">Yoruba</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<label for="taskDropdown">Select task:</label>
|
||||
<select id="taskDropdown" disabled>
|
||||
<select id="taskDropdown" >
|
||||
<option value="">Select Task</option>
|
||||
<option value="transcribe" selected>Transcribe</option>
|
||||
<option value="translate">Translate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<label for="modelSizeDropdown">Select Model Size:</label>
|
||||
<select id="modelSizeDropdown">
|
||||
<option value="">Select model</option>
|
||||
<option value="tiny">Tiny </option>
|
||||
<option value="tiny.en">Tiny (English-only)</option>
|
||||
<option value="base">Base</option>
|
||||
<option value="base.en">Base (English-only)</option>
|
||||
<option value="small" selected>Small</option>
|
||||
<option value="small.en">Small (English-only)</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="medium.en">Medium (English-only)</option>
|
||||
<option value="large-v2">Large-v2</option>
|
||||
<option value="large-v3">Large-v3</option>
|
||||
</select>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,11 +4,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const stopButton = document.getElementById("stopCapture");
|
||||
|
||||
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
||||
const useMultilingualCheckbox = document.getElementById('useMultilingualCheckbox');
|
||||
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
||||
const languageDropdown = document.getElementById('languageDropdown');
|
||||
const taskDropdown = document.getElementById('taskDropdown');
|
||||
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
||||
let selectedLanguage = null;
|
||||
let selectedTask = taskDropdown.value;
|
||||
let selectedModelSize = modelSizeDropdown.value;
|
||||
|
||||
// Add click event listeners to the buttons
|
||||
startButton.addEventListener("click", startCapture);
|
||||
@@ -30,11 +32,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.get("useMultilingualModelState", ({ useMultilingualModelState }) => {
|
||||
if (useMultilingualModelState !== undefined) {
|
||||
useMultilingualCheckbox.checked = useMultilingualModelState;
|
||||
languageDropdown.disabled = !useMultilingualModelState;
|
||||
taskDropdown.disabled = !useMultilingualModelState;
|
||||
chrome.storage.local.get("useVadState", ({ useVadState }) => {
|
||||
if (useVadState !== undefined) {
|
||||
useVadCheckbox.checked = useVadState;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,6 +52,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.local.get("selectedModelSize", ({ selectedModelSize: storedModelSize }) => {
|
||||
if (storedModelSize !== undefined) {
|
||||
modelSizeDropdown.value = storedModelSize;
|
||||
selectedModelSize = storedModelSize;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to handle the start capture button click event
|
||||
async function startCapture() {
|
||||
// Ignore click if the button is disabled
|
||||
@@ -77,9 +84,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
tabId: currentTab.id,
|
||||
host: host,
|
||||
port: port,
|
||||
useMultilingual: useMultilingualCheckbox.checked,
|
||||
language: selectedLanguage,
|
||||
task: selectedTask
|
||||
task: selectedTask,
|
||||
modelSize: selectedModelSize,
|
||||
useVad: useVadCheckbox.checked,
|
||||
}, () => {
|
||||
// Update capturing state in storage and toggle the buttons
|
||||
chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => {
|
||||
@@ -118,9 +126,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
function toggleCaptureButtons(isCapturing) {
|
||||
startButton.disabled = isCapturing;
|
||||
stopButton.disabled = !isCapturing;
|
||||
useServerCheckbox.disabled = isCapturing;
|
||||
useMultilingualCheckbox.disabled = isCapturing;
|
||||
|
||||
useServerCheckbox.disabled = isCapturing;
|
||||
useVadCheckbox.disabled = isCapturing;
|
||||
modelSizeDropdown.disabled = isCapturing;
|
||||
languageDropdown.disabled = isCapturing;
|
||||
taskDropdown.disabled = isCapturing;
|
||||
startButton.classList.toggle("disabled", isCapturing);
|
||||
stopButton.classList.toggle("disabled", !isCapturing);
|
||||
}
|
||||
@@ -131,16 +141,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
chrome.storage.local.set({ useServerState });
|
||||
});
|
||||
|
||||
useMultilingualCheckbox.addEventListener('change', function() {
|
||||
const useMultilingualModelState = useMultilingualCheckbox.checked;
|
||||
if (useMultilingualModelState) {
|
||||
languageDropdown.disabled = false;
|
||||
taskDropdown.disabled = false;
|
||||
} else {
|
||||
languageDropdown.disabled = true;
|
||||
taskDropdown.disabled = true;
|
||||
}
|
||||
chrome.storage.local.set({ useMultilingualModelState });
|
||||
useVadCheckbox.addEventListener("change", () => {
|
||||
const useVadState = useVadCheckbox.checked;
|
||||
chrome.storage.local.set({ useVadState });
|
||||
});
|
||||
|
||||
languageDropdown.addEventListener('change', function() {
|
||||
@@ -157,6 +160,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
chrome.storage.local.set({ selectedTask });
|
||||
});
|
||||
|
||||
modelSizeDropdown.addEventListener('change', function() {
|
||||
selectedModelSize = modelSizeDropdown.value;
|
||||
chrome.storage.local.set({ selectedModelSize });
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
||||
if (request.action === "updateSelectedLanguage") {
|
||||
const detectedLanguage = request.detectedLanguage;
|
||||
|
||||
@@ -24,9 +24,9 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
|
||||
### Options
|
||||
When using the Audio Transcription extension, you have the following options:
|
||||
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
||||
- **Use Multilingual Model**: Enable this option to utilize the multilingual capabilities of OpenAI-whisper.
|
||||
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
||||
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
||||
- **Model Size**: Select the whisper model size to run the server with.
|
||||
|
||||
### Getting Started
|
||||
- Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live).
|
||||
|
||||
@@ -66,18 +66,16 @@ function resampleTo16kHZ(audioData, origSampleRate = 44100) {
|
||||
function startRecording(data) {
|
||||
socket = new WebSocket(`ws://${data.host}:${data.port}/`);
|
||||
language = data.language;
|
||||
if (language === null && !data.useMultilingual) {
|
||||
language = 'en';
|
||||
}
|
||||
|
||||
const uuid = generateUUID();
|
||||
socket.onopen = function(e) {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
uid: uuid,
|
||||
multilingual: data.useMultilingual,
|
||||
language: data.language,
|
||||
task: data.task
|
||||
task: data.task,
|
||||
model: data.modelSize,
|
||||
use_vad: data.useVad
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -200,7 +198,7 @@ function init_element() {
|
||||
|
||||
elem_container = document.createElement('div');
|
||||
elem_container.id = "transcription";
|
||||
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;top:0px;position:absolute;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
|
||||
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;position:fixed;top:85%;left:50%;transform:translate(-50%,-50%);width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
|
||||
|
||||
for (var i = 0; i < 4; i++) {
|
||||
elem_text = document.createElement('span');
|
||||
|
||||
@@ -15,114 +15,114 @@
|
||||
<input type="checkbox" id="useServerCheckbox">
|
||||
<label for="useServerCheckbox">Use Collabora Whisper-Live Server</label>
|
||||
</div>
|
||||
<textarea id="waitTextBox" style="display: none;"></textarea>
|
||||
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="useMultilingualCheckbox">
|
||||
<label for="useMultilingualCheckbox">Use Multilingual Model</label>
|
||||
<input type="checkbox" id="useVadCheckbox">
|
||||
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
||||
</div>
|
||||
<textarea id="waitTextBox" style="display: none;"></textarea>
|
||||
<div class="dropdown-container">
|
||||
<label for="languageDropdown">Select Language:</label>
|
||||
<select id="languageDropdown" disabled>
|
||||
<option value="">Select Language</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="ta">Tamil</option>
|
||||
<option value="no">Norwegian</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="la">Latin</option>
|
||||
<option value="mi">Maori</option>
|
||||
<option value="ml">Malayalam</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="te">Telugu</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="az">Azerbaijani</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="kn">Kannada</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="mk">Macedonian</option>
|
||||
<option value="br">Breton</option>
|
||||
<option value="eu">Basque</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="hy">Armenian</option>
|
||||
<option value="ne">Nepali</option>
|
||||
<option value="mn">Mongolian</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="kk">Kazakh</option>
|
||||
<option value="sq">Albanian</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="gl">Galician</option>
|
||||
<option value="mr">Marathi</option>
|
||||
<option value="pa">Punjabi</option>
|
||||
<option value="si">Sinhala</option>
|
||||
<option value="km">Khmer</option>
|
||||
<option value="sn">Shona</option>
|
||||
<option value="yo">Yoruba</option>
|
||||
<option value="so">Somali</option>
|
||||
<select id="languageDropdown">
|
||||
<option value="" selected>Automatically detect</option>
|
||||
<option value="af">Afrikaans</option>
|
||||
<option value="oc">Occitan</option>
|
||||
<option value="ka">Georgian</option>
|
||||
<option value="be">Belarusian</option>
|
||||
<option value="tg">Tajik</option>
|
||||
<option value="sd">Sindhi</option>
|
||||
<option value="gu">Gujarati</option>
|
||||
<option value="sq">Albanian</option>
|
||||
<option value="am">Amharic</option>
|
||||
<option value="yi">Yiddish</option>
|
||||
<option value="lo">Lao</option>
|
||||
<option value="uz">Uzbek</option>
|
||||
<option value="fo">Faroese</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="ps">Pashto</option>
|
||||
<option value="tk">Turkmen</option>
|
||||
<option value="nn">Nynorsk</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="sa">Sanskrit</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="my">Myanmar</option>
|
||||
<option value="bo">Tibetan</option>
|
||||
<option value="tl">Tagalog</option>
|
||||
<option value="mg">Malagasy</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="hy">Armenian</option>
|
||||
<option value="as">Assamese</option>
|
||||
<option value="tt">Tatar</option>
|
||||
<option value="haw">Hawaiian</option>
|
||||
<option value="ln">Lingala</option>
|
||||
<option value="ha">Hausa</option>
|
||||
<option value="az">Azerbaijani</option>
|
||||
<option value="ba">Bashkir</option>
|
||||
<option value="eu">Basque</option>
|
||||
<option value="be">Belarusian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="br">Breton</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="zh">Chinese</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="fo">Faroese</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="gl">Galician</option>
|
||||
<option value="ka">Georgian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="gu">Gujarati</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="ha">Hausa</option>
|
||||
<option value="haw">Hawaiian</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="jw">Javanese</option>
|
||||
<option value="kn">Kannada</option>
|
||||
<option value="kk">Kazakh</option>
|
||||
<option value="km">Khmer</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="lo">Lao</option>
|
||||
<option value="la">Latin</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="ln">Lingala</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="lb">Luxembourgish</option>
|
||||
<option value="mk">Macedonian</option>
|
||||
<option value="mg">Malagasy</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="ml">Malayalam</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="mi">Maori</option>
|
||||
<option value="mr">Marathi</option>
|
||||
<option value="mn">Mongolian</option>
|
||||
<option value="my">Myanmar</option>
|
||||
<option value="ne">Nepali</option>
|
||||
<option value="no">Norwegian</option>
|
||||
<option value="nn">Nynorsk</option>
|
||||
<option value="oc">Occitan</option>
|
||||
<option value="ps">Pashto</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="pa">Punjabi</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="sa">Sanskrit</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="sn">Shona</option>
|
||||
<option value="sd">Sindhi</option>
|
||||
<option value="si">Sinhala</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="so">Somali</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="su">Sundanese</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="tl">Tagalog</option>
|
||||
<option value="tg">Tajik</option>
|
||||
<option value="ta">Tamil</option>
|
||||
<option value="tt">Tatar</option>
|
||||
<option value="te">Telugu</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="bo">Tibetan</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="tk">Turkmen</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="uz">Uzbek</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="yi">Yiddish</option>
|
||||
<option value="yo">Yoruba</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
@@ -133,5 +133,21 @@
|
||||
<option value="translate">Translate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<label for="modelSizeDropdown">Select Model Size:</label>
|
||||
<select id="modelSizeDropdown">
|
||||
<option value="">Select model</option>
|
||||
<option value="tiny">Tiny </option>
|
||||
<option value="tiny.en">Tiny (English-only)</option>
|
||||
<option value="base">Base</option>
|
||||
<option value="base.en">Base (English-only)</option>
|
||||
<option value="small" selected>Small</option>
|
||||
<option value="small.en">Small (English-only)</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="medium.en">Medium (English-only)</option>
|
||||
<option value="large-v2">Large-v2</option>
|
||||
<option value="large-v3">Large-v3</option>
|
||||
</select>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -3,11 +3,14 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
const stopButton = document.getElementById("stopCapture");
|
||||
|
||||
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
||||
const useMultilingualCheckbox = document.getElementById('useMultilingualCheckbox');
|
||||
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
||||
const languageDropdown = document.getElementById('languageDropdown');
|
||||
const taskDropdown = document.getElementById('taskDropdown');
|
||||
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
||||
let selectedLanguage = null;
|
||||
let selectedTask = taskDropdown.value;
|
||||
let selectedModelSize = modelSizeDropdown.value;
|
||||
|
||||
|
||||
browser.storage.local.get("capturingState")
|
||||
.then(function(result) {
|
||||
@@ -32,11 +35,9 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
}
|
||||
});
|
||||
|
||||
browser.storage.local.get("useMultilingualModelState", ({ useMultilingualModelState }) => {
|
||||
if (useMultilingualModelState !== undefined) {
|
||||
useMultilingualCheckbox.checked = useMultilingualModelState;
|
||||
languageDropdown.disabled = !useMultilingualModelState;
|
||||
taskDropdown.disabled = !useMultilingualModelState;
|
||||
browser.storage.local.get("useVadState", ({ useVadState }) => {
|
||||
if (useVadState !== undefined) {
|
||||
useVadCheckbox.checked = useVadState;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,6 +55,13 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
}
|
||||
});
|
||||
|
||||
browser.storage.local.get("selectedModelSize", ({ selectedModelSize: storedModelSize }) => {
|
||||
if (storedModelSize !== undefined) {
|
||||
modelSizeDropdown.value = storedModelSize;
|
||||
selectedModelSize = storedModelSize;
|
||||
}
|
||||
});
|
||||
|
||||
startButton.addEventListener("click", function() {
|
||||
let host = "localhost";
|
||||
let port = "9090";
|
||||
@@ -73,9 +81,10 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
data: {
|
||||
host: host,
|
||||
port: port,
|
||||
useMultilingual: useMultilingualCheckbox.checked,
|
||||
language: selectedLanguage,
|
||||
task: selectedTask
|
||||
task: selectedTask,
|
||||
modelSize: selectedModelSize,
|
||||
useVad: useVadCheckbox.checked,
|
||||
}
|
||||
});
|
||||
toggleCaptureButtons(true);
|
||||
@@ -114,8 +123,10 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
startButton.disabled = isCapturing;
|
||||
stopButton.disabled = !isCapturing;
|
||||
useServerCheckbox.disabled = isCapturing;
|
||||
useMultilingualCheckbox.disabled = isCapturing;
|
||||
|
||||
useVadCheckbox.disabled = isCapturing;
|
||||
modelSizeDropdown.disabled = isCapturing;
|
||||
languageDropdown.disabled = isCapturing;
|
||||
taskDropdown.disabled = isCapturing;
|
||||
startButton.classList.toggle("disabled", isCapturing);
|
||||
stopButton.classList.toggle("disabled", !isCapturing);
|
||||
}
|
||||
@@ -126,16 +137,9 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
browser.storage.local.set({ useServerState });
|
||||
});
|
||||
|
||||
useMultilingualCheckbox.addEventListener('change', function() {
|
||||
const useMultilingualModelState = useMultilingualCheckbox.checked;
|
||||
if (useMultilingualModelState) {
|
||||
languageDropdown.disabled = false;
|
||||
taskDropdown.disabled = false;
|
||||
} else {
|
||||
languageDropdown.disabled = true;
|
||||
taskDropdown.disabled = true;
|
||||
}
|
||||
browser.storage.local.set({ useMultilingualModelState });
|
||||
useVadCheckbox.addEventListener("change", () => {
|
||||
const useVadState = useVadCheckbox.checked;
|
||||
browser.storage.local.set({ useVadState });
|
||||
});
|
||||
|
||||
languageDropdown.addEventListener('change', function() {
|
||||
@@ -152,6 +156,11 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
browser.storage.local.set({ selectedTask });
|
||||
});
|
||||
|
||||
modelSizeDropdown.addEventListener('change', function() {
|
||||
selectedModelSize = modelSizeDropdown.value;
|
||||
browser.storage.local.set({ selectedModelSize });
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "updateSelectedLanguage") {
|
||||
const detectedLanguage = request.data;
|
||||
|
||||
@@ -108,4 +108,4 @@ label {
|
||||
|
||||
.dropdown-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
# whisper-live
|
||||
A nearly-live implementation of OpenAI's Whisper.
|
||||
# WhisperLive
|
||||
|
||||
This project is a real-time transcription application that uses the OpenAI Whisper model to convert speech input into text output. It can be used to transcribe both live audio input from microphone and pre-recorded audio files.
|
||||
<h2 align="center">
|
||||
<a href="https://www.youtube.com/watch?v=0PHWCApIcCI"><img
|
||||
src="https://img.youtube.com/vi/0PHWCApIcCI/0.jpg" style="background-color:rgba(0,0,0,0);" height=300 alt="WhisperLive"></a>
|
||||
<br><br>A nearly-live implementation of OpenAI's Whisper.
|
||||
<br><br>
|
||||
</h2>
|
||||
|
||||
Unlike traditional speech recognition systems that rely on continuous audio streaming, we use [voice activity detection (VAD)](https://github.com/snakers4/silero-vad) to detect the presence of speech and only send the audio data to whisper when speech is detected. This helps to reduce the amount of data sent to the whisper model and improves the accuracy of the transcription output.
|
||||
This project is a real-time transcription application that uses the OpenAI Whisper model
|
||||
to convert speech input into text output. It can be used to transcribe both live audio
|
||||
input from microphone and pre-recorded audio files.
|
||||
|
||||
## Installation
|
||||
- Install PyAudio and ffmpeg
|
||||
```bash
|
||||
bash setup.sh
|
||||
bash scripts/setup.sh
|
||||
```
|
||||
|
||||
- Install whisper-live from pip
|
||||
@@ -16,64 +22,91 @@ Unlike traditional speech recognition systems that rely on continuous audio stre
|
||||
pip install whisper-live
|
||||
```
|
||||
|
||||
### Setting up NVIDIA/TensorRT-LLM for TensorRT backend
|
||||
- Please follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) for setup of [NVIDIA/TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) and for building Whisper-TensorRT engine.
|
||||
|
||||
## Getting Started
|
||||
- Run the server
|
||||
```python
|
||||
from whisper_live.server import TranscriptionServer
|
||||
server = TranscriptionServer()
|
||||
server.run("0.0.0.0", 9090)
|
||||
The server supports two backends `faster_whisper` and `tensorrt`. If running `tensorrt` backend follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md)
|
||||
|
||||
### Running the Server
|
||||
- [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) backend
|
||||
```bash
|
||||
python3 run_server.py --port 9090 \
|
||||
--backend faster_whisper
|
||||
|
||||
# running with custom model
|
||||
python3 run_server.py --port 9090 \
|
||||
--backend faster_whisper
|
||||
-fw "/path/to/custom/faster/whisper/model"
|
||||
```
|
||||
|
||||
- On the client side
|
||||
- To transcribe an audio file:
|
||||
```python
|
||||
from whisper_live.client import TranscriptionClient
|
||||
client = TranscriptionClient("localhost", 9090, is_multilingual=True, lang="hi", translate=True)
|
||||
client(audio_file_path)
|
||||
```
|
||||
This command transcribes the specified audio file (audio.wav) using the Whisper model. It connects to the server running on localhost at port 9090. It also enables the multilingual feature, allowing transcription in multiple languages. The language option specifies the target language for transcription, in this case, Hindi ("hi"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language.
|
||||
- TensorRT backend. Currently, we recommend to only use the docker setup for TensorRT. Follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) which works as expected. Make sure to build your TensorRT Engines before running the server with TensorRT backend.
|
||||
```bash
|
||||
# Run English only model
|
||||
python3 run_server.py -p 9090 \
|
||||
-b tensorrt \
|
||||
-trt /home/TensorRT-LLM/examples/whisper/whisper_small_en
|
||||
|
||||
- To transcribe from microphone:
|
||||
```python
|
||||
from whisper_live.client import TranscriptionClient
|
||||
client = TranscriptionClient(host, port, is_multilingual=True, lang="hi", translate=True)
|
||||
client()
|
||||
```
|
||||
This command captures audio from the microphone and sends it to the server for transcription. It uses the same options as the previous command, enabling the multilingual feature and specifying the target language and task.
|
||||
|
||||
|
||||
## Transcribe audio from browser
|
||||
- Run the server
|
||||
```python
|
||||
from whisper_live.server import TranscriptionServer
|
||||
server = TranscriptionServer()
|
||||
server.run("0.0.0.0", 9090)
|
||||
# Run Multilingual model
|
||||
python3 run_server.py -p 9090 \
|
||||
-b tensorrt \
|
||||
-trt /home/TensorRT-LLM/examples/whisper/whisper_small \
|
||||
-m
|
||||
```
|
||||
This would start the websocket server on port ```9090```.
|
||||
|
||||
### Chrome Extension
|
||||
- Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) to use Chrome extension.
|
||||
|
||||
### Firefox Extension
|
||||
- Refer to [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) to use Mozilla Firefox extension.
|
||||
### Running the Client
|
||||
- Initializing the client:
|
||||
```python
|
||||
from whisper_live.client import TranscriptionClient
|
||||
client = TranscriptionClient(
|
||||
"localhost",
|
||||
9090,
|
||||
lang="en",
|
||||
translate=False,
|
||||
model="small",
|
||||
use_vad=False,
|
||||
)
|
||||
```
|
||||
It connects to the server running on localhost at port 9090. Using a multilingual model, language for the transcription will be automatically detected. You can also use the language option to specify the target language for the transcription, in this case, English ("en"). The translate option should be set to `True` if we want to translate from the source language to English and `False` if we want to transcribe in the source language.
|
||||
|
||||
- Trancribe an audio file:
|
||||
```python
|
||||
client("tests/jfk.wav")
|
||||
```
|
||||
|
||||
- To transcribe from microphone:
|
||||
```python
|
||||
client()
|
||||
```
|
||||
|
||||
- To transcribe from a HLS stream:
|
||||
```python
|
||||
client(hls_url="http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_1xtra/bbc_1xtra.isml/bbc_1xtra-audio%3d96000.norewind.m3u8")
|
||||
```
|
||||
|
||||
## Browser Extensions
|
||||
- Run the server with your desired backend as shown [here](https://github.com/collabora/WhisperLive?tab=readme-ov-file#running-the-server).
|
||||
- Transcribe audio directly from your browser using our Chrome or Firefox extensions. Refer to [Audio-Transcription-Chrome](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Chrome#readme) and [Audio-Transcription-Firefox](https://github.com/collabora/whisper-live/tree/main/Audio-Transcription-Firefox#readme) for setup instructions.
|
||||
|
||||
## Whisper Live Server in Docker
|
||||
- GPU
|
||||
```bash
|
||||
docker build . -t whisper-live -f docker/Dockerfile.gpu
|
||||
docker run -it --gpus all -p 9090:9090 whisper-live:latest
|
||||
```
|
||||
- Faster-Whisper
|
||||
```bash
|
||||
docker run -it --gpus all -p 9090:9090 ghcr.io/collabora/whisperlive-gpu:latest
|
||||
```
|
||||
|
||||
- TensorRT. Follow [TensorRT_whisper readme](https://github.com/collabora/WhisperLive/blob/main/TensorRT_whisper.md) in order to setup docker and use TensorRT backend. We provide a pre-built docker image which has TensorRT-LLM built and ready to use.
|
||||
|
||||
- CPU
|
||||
```bash
|
||||
docker build . -t whisper-live -f docker/Dockerfile.cpu
|
||||
docker run -it -p 9090:9090 whisper-live:latest
|
||||
docker run -it -p 9090:9090 ghcr.io/collabora/whisperlive-cpu:latest
|
||||
```
|
||||
**Note**: By default we use "small" model size. To build docker image for a different model size, change the size in server.py and then build the docker image.
|
||||
|
||||
## Future Work
|
||||
- [ ] Add translation to other languages on top of transcription.
|
||||
- [ ] TensorRT backend for Whisper.
|
||||
- [x] TensorRT backend for Whisper.
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -98,6 +131,5 @@ We are available to help you with both Open Source and proprietary AI projects.
|
||||
publisher = {GitHub},
|
||||
journal = {GitHub repository},
|
||||
howpublished = {\url{https://github.com/snakers4/silero-vad}},
|
||||
commit = {insert_some_commit_here},
|
||||
email = {hello@silero.ai}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Whisper-TensorRT
|
||||
We have only tested the TensorRT backend in docker so, we recommend docker for a smooth TensorRT backend setup.
|
||||
**Note**: We use [our fork to setup TensorRT](https://github.com/makaveli10/TensorRT-LLM)
|
||||
|
||||
## Installation
|
||||
- Install [docker](https://docs.docker.com/engine/install/)
|
||||
- Install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
|
||||
|
||||
- Clone this repo.
|
||||
```bash
|
||||
git clone https://github.com/collabora/WhisperLive.git
|
||||
cd WhisperLive
|
||||
```
|
||||
|
||||
- Pull the TensorRT-LLM docker image which we prebuilt for WhisperLive TensorRT backend.
|
||||
```bash
|
||||
docker pull ghcr.io/collabora/whisperbot-base:latest
|
||||
```
|
||||
|
||||
- Next, we run the docker image and mount WhisperLive repo to the containers `/home` directory.
|
||||
```bash
|
||||
docker run -it --gpus all --shm-size=8g \
|
||||
--ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \
|
||||
-p 9090:9090 -v /path/to/WhisperLive:/home/WhisperLive \
|
||||
ghcr.io/collabora/whisperbot-base:latest
|
||||
```
|
||||
|
||||
- Make sure to test the installation.
|
||||
```bash
|
||||
# export ENV=${ENV:-/etc/shinit_v2}
|
||||
# source $ENV
|
||||
python -c "import torch; import tensorrt; import tensorrt_llm"
|
||||
```
|
||||
**NOTE**: Uncomment and update library paths if imports fail.
|
||||
|
||||
## Whisper TensorRT Engine
|
||||
- We build `small.en` and `small` multilingual TensorRT engine. The script logs the path of the directory with Whisper TensorRT engine. We need the model_path to run the server.
|
||||
```bash
|
||||
# convert small.en
|
||||
bash scripts/build_whisper_tensorrt.sh /root/TensorRT-LLM-examples small.en
|
||||
|
||||
# convert small multilingual model
|
||||
bash scripts/build_whisper_tensorrt.sh /root/TensorRT-LLM-examples small
|
||||
```
|
||||
|
||||
## Run WhisperLive Server with TensorRT Backend
|
||||
```bash
|
||||
cd /home/WhisperLive
|
||||
|
||||
# Install requirements
|
||||
apt update && bash scripts/setup.sh
|
||||
pip install -r requirements/server.txt
|
||||
|
||||
# Required to create mel spectogram
|
||||
wget --directory-prefix=assets assets/mel_filters.npz https://raw.githubusercontent.com/openai/whisper/main/whisper/assets/mel_filters.npz
|
||||
|
||||
# Run English only model
|
||||
python3 run_server.py --port 9090 \
|
||||
--backend tensorrt \
|
||||
--trt_model_path "path/to/whisper_trt/from/build/step"
|
||||
|
||||
# Run Multilingual model
|
||||
python3 run_server.py --port 9090 \
|
||||
--backend tensorrt \
|
||||
--trt_model_path "path/to/whisper_trt/from/build/step" \
|
||||
--trt_multilingual
|
||||
```
|
||||
Binary file not shown.
+6
-27
@@ -1,45 +1,24 @@
|
||||
FROM ubuntu:focal
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Remove any third-party apt sources to avoid issues with expiring keys.
|
||||
RUN rm -f /etc/apt/sources.list.d/*.list
|
||||
|
||||
# Install some basic utilities.
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
git \
|
||||
bzip2 \
|
||||
libx11-6 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt update
|
||||
|
||||
# install python
|
||||
RUN apt install software-properties-common -y && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt update
|
||||
|
||||
RUN apt install python3-dev -y && \
|
||||
apt install python-is-python3
|
||||
|
||||
|
||||
# install pip
|
||||
RUN apt install python3-pip -y
|
||||
|
||||
# Create a working directory.
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY setup.sh /app
|
||||
COPY requirements/ /app
|
||||
COPY scripts/setup.sh requirements/server.txt /app/
|
||||
|
||||
RUN bash setup.sh
|
||||
RUN pip install -r server.txt
|
||||
RUN apt update && bash setup.sh && pip install -r server.txt
|
||||
|
||||
COPY whisper_live /app/whisper_live
|
||||
|
||||
COPY run_server.py /app
|
||||
|
||||
CMD ["python", "run_server.py"]
|
||||
|
||||
+9
-23
@@ -1,44 +1,30 @@
|
||||
FROM nvidia/cuda:11.2.2-cudnn8-runtime-ubuntu20.04
|
||||
|
||||
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Remove any third-party apt sources to avoid issues with expiring keys.
|
||||
RUN rm -f /etc/apt/sources.list.d/*.list
|
||||
|
||||
# Install some basic utilities.
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
git \
|
||||
bzip2 \
|
||||
libx11-6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt update
|
||||
|
||||
# install python
|
||||
RUN apt install software-properties-common -y && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt update
|
||||
|
||||
RUN apt install python3-dev -y && \
|
||||
apt install python-is-python3
|
||||
|
||||
|
||||
# install pip
|
||||
RUN apt install python3-pip -y
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
&& python3 -m pip install --upgrade pip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a working directory.
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY setup.sh /app
|
||||
COPY requirements/ /app
|
||||
COPY scripts/setup.sh requirements/server.txt /app
|
||||
|
||||
RUN apt update --fix-missing
|
||||
RUN bash setup.sh
|
||||
RUN pip install -r server.txt
|
||||
RUN apt update && bash setup.sh && rm setup.sh
|
||||
RUN pip install -r server.txt && rm server.txt
|
||||
|
||||
COPY whisper_live /app/whisper_live
|
||||
|
||||
|
||||
+11
-6
@@ -1,7 +1,12 @@
|
||||
PyAudio
|
||||
faster-whisper==0.6.0
|
||||
--extra-index-url https://download.pytorch.org/whl/cu111
|
||||
torch==1.10.1
|
||||
torchaudio==0.10.1
|
||||
faster-whisper==0.10.0
|
||||
torch
|
||||
websockets
|
||||
onnxruntime==1.16.0
|
||||
onnxruntime==1.16.0
|
||||
numba
|
||||
openai-whisper
|
||||
kaldialign
|
||||
soundfile
|
||||
ffmpeg-python
|
||||
scipy
|
||||
jiwer
|
||||
evaluate
|
||||
+34
-1
@@ -1,5 +1,38 @@
|
||||
import argparse
|
||||
from whisper_live.server import TranscriptionServer
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', '-p',
|
||||
type=int,
|
||||
default=9090,
|
||||
help="Websocket port to run the server on.")
|
||||
parser.add_argument('--backend', '-b',
|
||||
type=str,
|
||||
default='faster_whisper',
|
||||
help='Backends from ["tensorrt", "faster_whisper"]')
|
||||
parser.add_argument('--faster_whisper_custom_model_path', '-fw',
|
||||
type=str, default=None,
|
||||
help="Custom Faster Whisper Model")
|
||||
parser.add_argument('--trt_model_path', '-trt',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Whisper TensorRT model path')
|
||||
parser.add_argument('--trt_multilingual', '-m',
|
||||
action="store_true",
|
||||
help='Boolean only for TensorRT model. True if multilingual.')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.backend == "tensorrt":
|
||||
if args.trt_model_path is None:
|
||||
raise ValueError("Please Provide a valid tensorrt model path")
|
||||
|
||||
server = TranscriptionServer()
|
||||
server.run("0.0.0.0")
|
||||
server.run(
|
||||
"0.0.0.0",
|
||||
port=args.port,
|
||||
backend=args.backend,
|
||||
faster_whisper_custom_model_path=args.faster_whisper_custom_model_path,
|
||||
whisper_tensorrt_path=args.trt_model_path,
|
||||
trt_multilingual=args.trt_multilingual
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
download_and_build_model() {
|
||||
local model_name="$1"
|
||||
local model_url=""
|
||||
|
||||
case "$model_name" in
|
||||
"tiny.en")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/d3dd57d32accea0b295c96e26691aa14d8822fac7d9d27d5dc00b4ca2826dd03/tiny.en.pt"
|
||||
;;
|
||||
"tiny")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt"
|
||||
;;
|
||||
"base.en")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/25a8566e1d0c1e2231d1c762132cd20e0f96a85d16145c3a00adf5d1ac670ead/base.en.pt"
|
||||
;;
|
||||
"base")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/ed3a0b6b1c0edf879ad9b11b1af5a0e6ab5db9205f891f668f8b0e6c6326e34e/base.pt"
|
||||
;;
|
||||
"small.en")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/f953ad0fd29cacd07d5a9eda5624af0f6bcf2258be67c92b79389873d91e0872/small.en.pt"
|
||||
;;
|
||||
"small")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/9ecf779972d90ba49c06d968637d720dd632c55bbf19d441fb42bf17a411e794/small.pt"
|
||||
;;
|
||||
"medium.en")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/d7440d1dc186f76616474e0ff0b3b6b879abc9d1a4926b7adfa41db2d497ab4f/medium.en.pt"
|
||||
;;
|
||||
"medium")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/345ae4da62f9b3d59415adc60127b97c714f32e89e936602e85993674d08dcb1/medium.pt"
|
||||
;;
|
||||
"large-v1")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/e4b87e7e0bf463eb8e6956e646f1e277e901512310def2c24bf0e11bd3c28e9a/large-v1.pt"
|
||||
;;
|
||||
"large-v2")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/81f7c96c852ee8fc832187b0132e569d6c3065a3252ed18e56effd0b6a73e524/large-v2.pt"
|
||||
;;
|
||||
"large-v3" | "large")
|
||||
model_url="https://openaipublic.azureedge.net/main/whisper/models/e5b1a55b89c1367dacf97e3e19bfd829a01529dbfdeefa8caeb59b3f1b81dadb/large-v3.pt"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid model name: $model_name"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading $model_name..."
|
||||
# wget --directory-prefix=assets "$model_url"
|
||||
# echo "Download completed: ${model_name}.pt"
|
||||
if [ ! -f "assets/${model_name}.pt" ]; then
|
||||
wget --directory-prefix=assets "$model_url"
|
||||
echo "Download completed: ${model_name}.pt"
|
||||
else
|
||||
echo "${model_name}.pt already exists in assets directory."
|
||||
fi
|
||||
|
||||
local output_dir="whisper_${model_name//./_}"
|
||||
echo "$output_dir"
|
||||
echo "Running build script for $model_name with output directory $output_dir"
|
||||
python3 build.py --output_dir "$output_dir" --use_gpt_attention_plugin --use_gemm_plugin --use_bert_attention_plugin --model_name "$model_name"
|
||||
echo "Whisper $model_name TensorRT engine built."
|
||||
echo "========================================="
|
||||
echo "Model is located at: $(pwd)/$output_dir"
|
||||
}
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: $0 <path-to-tensorrt-examples-dir> [model-name]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tensorrt_examples_dir="$1"
|
||||
model_name="${2:-small.en}"
|
||||
|
||||
cd $1/whisper
|
||||
pip install --no-deps -r requirements.txt
|
||||
|
||||
download_and_build_model "$model_name"
|
||||
@@ -10,45 +10,51 @@ HERE = pathlib.Path(__file__).parent
|
||||
README = (HERE / "README.md").read_text()
|
||||
|
||||
# This call to setup() does all the work
|
||||
setup(name="whisper-live",
|
||||
version=__version__,
|
||||
description="A nearly-live implementation of OpenAI's Whisper.",
|
||||
long_description=README,
|
||||
long_description_content_type="text/markdown",
|
||||
include_package_data=True,
|
||||
url="https://github.com/collabora/WhisperLive",
|
||||
author="Collabora Ltd",
|
||||
author_email="vineet.suryan@collabora.com",
|
||||
license="MIT",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Science/Research",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
],
|
||||
packages=find_packages(
|
||||
exclude=("examples",
|
||||
"Audio-Transcription-Chrome",
|
||||
"Audio-Transcription-Firefox",
|
||||
"requirements",
|
||||
"whisper-finetuning"
|
||||
)
|
||||
),
|
||||
install_requires=[
|
||||
setup(
|
||||
name="whisper-live",
|
||||
version=__version__,
|
||||
description="A nearly-live implementation of OpenAI's Whisper.",
|
||||
long_description=README,
|
||||
long_description_content_type="text/markdown",
|
||||
include_package_data=True,
|
||||
url="https://github.com/collabora/WhisperLive",
|
||||
author="Collabora Ltd",
|
||||
author_email="vineet.suryan@collabora.com",
|
||||
license="MIT",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Science/Research",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
],
|
||||
packages=find_packages(
|
||||
exclude=(
|
||||
"examples",
|
||||
"Audio-Transcription-Chrome",
|
||||
"Audio-Transcription-Firefox",
|
||||
"requirements",
|
||||
"whisper-finetuning"
|
||||
)
|
||||
),
|
||||
install_requires=[
|
||||
"PyAudio",
|
||||
"faster-whisper==0.6.0",
|
||||
"faster-whisper==0.10.0",
|
||||
"torch",
|
||||
"torchaudio",
|
||||
"websockets",
|
||||
"onnxruntime",
|
||||
"onnxruntime==1.16.0",
|
||||
"ffmpeg-python",
|
||||
"scipy",
|
||||
"websocket-client",
|
||||
],
|
||||
python_requires=">=3.8"
|
||||
)
|
||||
"numba",
|
||||
"openai-whisper",
|
||||
"kaldialign",
|
||||
"soundfile",
|
||||
],
|
||||
python_requires=">=3.8"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import json
|
||||
import os
|
||||
import scipy
|
||||
import websocket
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from whisper_live.client import TranscriptionClient
|
||||
from whisper_live.utils import resample
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
@patch('whisper_live.client.websocket.WebSocketApp')
|
||||
@patch('whisper_live.client.pyaudio.PyAudio')
|
||||
def setUp(self, mock_pyaudio, mock_websocket):
|
||||
self.mock_pyaudio_instance = MagicMock()
|
||||
mock_pyaudio.return_value = self.mock_pyaudio_instance
|
||||
self.mock_stream = MagicMock()
|
||||
self.mock_pyaudio_instance.open.return_value = self.mock_stream
|
||||
|
||||
self.mock_ws_app = mock_websocket.return_value
|
||||
self.mock_ws_app.send = MagicMock()
|
||||
|
||||
self.client = TranscriptionClient(host='localhost', port=9090, lang="en").client
|
||||
|
||||
self.mock_pyaudio = mock_pyaudio
|
||||
self.mock_websocket = mock_websocket
|
||||
|
||||
def tearDown(self):
|
||||
self.client.close_websocket()
|
||||
self.mock_pyaudio.stop()
|
||||
self.mock_websocket.stop()
|
||||
del self.client
|
||||
|
||||
|
||||
class TestClientWebSocketCommunication(BaseTestCase):
|
||||
def test_websocket_communication(self):
|
||||
expected_url = 'ws://localhost:9090'
|
||||
self.mock_websocket.assert_called()
|
||||
self.assertEqual(self.mock_websocket.call_args[0][0], expected_url)
|
||||
|
||||
|
||||
class TestClientCallbacks(BaseTestCase):
|
||||
def test_on_open(self):
|
||||
expected_message = json.dumps({
|
||||
"uid": self.client.uid,
|
||||
"language": self.client.language,
|
||||
"task": self.client.task,
|
||||
"model": self.client.model,
|
||||
"use_vad": True
|
||||
})
|
||||
self.client.on_open(self.mock_ws_app)
|
||||
self.mock_ws_app.send.assert_called_with(expected_message)
|
||||
|
||||
def test_on_message(self):
|
||||
message = json.dumps(
|
||||
{
|
||||
"uid": self.client.uid,
|
||||
"message": "SERVER_READY",
|
||||
"backend": "faster_whisper"
|
||||
}
|
||||
)
|
||||
self.client.on_message(self.mock_ws_app, message)
|
||||
|
||||
message = json.dumps({
|
||||
"uid": self.client.uid,
|
||||
"segments": [
|
||||
{"start": 0, "end": 1, "text": "Test transcript"},
|
||||
{"start": 1, "end": 2, "text": "Test transcript 2"},
|
||||
{"start": 2, "end": 3, "text": "Test transcript 3"}
|
||||
]
|
||||
})
|
||||
self.client.on_message(self.mock_ws_app, message)
|
||||
|
||||
# Assert that the transcript was updated correctly
|
||||
self.assertEqual(len(self.client.transcript), 2)
|
||||
self.assertEqual(self.client.transcript[1]['text'], "Test transcript 2")
|
||||
|
||||
def test_on_close(self):
|
||||
close_status_code = 1000
|
||||
close_msg = "Normal closure"
|
||||
self.client.on_close(self.mock_ws_app, close_status_code, close_msg)
|
||||
|
||||
self.assertFalse(self.client.recording)
|
||||
self.assertFalse(self.client.server_error)
|
||||
self.assertFalse(self.client.waiting)
|
||||
|
||||
def test_on_error(self):
|
||||
error_message = "Test Error"
|
||||
self.client.on_error(self.mock_ws_app, error_message)
|
||||
|
||||
self.assertTrue(self.client.server_error)
|
||||
self.assertEqual(self.client.error_message, error_message)
|
||||
|
||||
|
||||
class TestAudioResampling(unittest.TestCase):
|
||||
def test_resample_audio(self):
|
||||
original_audio = "assets/jfk.flac"
|
||||
expected_sr = 16000
|
||||
resampled_audio = resample(original_audio, expected_sr)
|
||||
|
||||
sr, _ = scipy.io.wavfile.read(resampled_audio)
|
||||
self.assertEqual(sr, expected_sr)
|
||||
|
||||
os.remove(resampled_audio)
|
||||
|
||||
|
||||
class TestSendingAudioPacket(BaseTestCase):
|
||||
def test_send_packet(self):
|
||||
mock_audio_packet = b'\x00\x01\x02\x03'
|
||||
self.client.send_packet_to_server(mock_audio_packet)
|
||||
self.client.client_socket.send.assert_called_with(mock_audio_packet, websocket.ABNF.OPCODE_BINARY)
|
||||
@@ -0,0 +1,137 @@
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import evaluate
|
||||
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from whisper_live.server import TranscriptionServer
|
||||
from whisper_live.client import TranscriptionClient
|
||||
from whisper.normalizers import EnglishTextNormalizer
|
||||
|
||||
|
||||
class TestTranscriptionServerInitialization(unittest.TestCase):
|
||||
def test_initialization(self):
|
||||
server = TranscriptionServer()
|
||||
self.assertEqual(server.client_manager.max_clients, 4)
|
||||
self.assertEqual(server.client_manager.max_connection_time, 600)
|
||||
self.assertDictEqual(server.client_manager.clients, {})
|
||||
self.assertDictEqual(server.client_manager.start_times, {})
|
||||
|
||||
|
||||
class TestGetWaitTime(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.server = TranscriptionServer()
|
||||
self.server.client_manager.start_times = {
|
||||
'client1': time.time() - 120,
|
||||
'client2': time.time() - 300
|
||||
}
|
||||
self.server.client_manager.max_connection_time = 600
|
||||
|
||||
def test_get_wait_time(self):
|
||||
expected_wait_time = (600 - (time.time() - self.server.client_manager.start_times['client2'])) / 60
|
||||
print(self.server.client_manager.get_wait_time(), expected_wait_time)
|
||||
self.assertAlmostEqual(self.server.client_manager.get_wait_time(), expected_wait_time, places=2)
|
||||
|
||||
|
||||
class TestServerConnection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.server = TranscriptionServer()
|
||||
|
||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||
def test_connection(self, mock_websocket):
|
||||
mock_websocket.recv.return_value = json.dumps({
|
||||
'uid': 'test_client',
|
||||
'language': 'en',
|
||||
'task': 'transcribe',
|
||||
'model': 'tiny.en'
|
||||
})
|
||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||
|
||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||
def test_recv_audio_exception_handling(self, mock_websocket):
|
||||
mock_websocket.recv.side_effect = [json.dumps({
|
||||
'uid': 'test_client',
|
||||
'language': 'en',
|
||||
'task': 'transcribe',
|
||||
'model': 'tiny.en'
|
||||
}), np.array([1, 2, 3]).tobytes()]
|
||||
|
||||
with self.assertLogs(level="ERROR"):
|
||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||
|
||||
self.assertNotIn(mock_websocket, self.server.client_manager.clients)
|
||||
|
||||
|
||||
class TestServerInferenceAccuracy(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server_process = subprocess.Popen(["python", "run_server.py"])
|
||||
time.sleep(2)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.server_process.terminate()
|
||||
cls.server_process.wait()
|
||||
|
||||
@mock.patch('pyaudio.PyAudio')
|
||||
def setUp(self, mock_pyaudio):
|
||||
self.mock_pyaudio = mock_pyaudio.return_value
|
||||
self.mock_stream = mock.MagicMock()
|
||||
self.mock_pyaudio.open.return_value = self.mock_stream
|
||||
self.metric = evaluate.load("wer")
|
||||
self.normalizer = EnglishTextNormalizer()
|
||||
self.client = TranscriptionClient(
|
||||
"localhost", "9090", model="base.en", lang="en",
|
||||
)
|
||||
|
||||
def test_inference(self):
|
||||
gt = "And so my fellow Americans, ask not, what your country can do for you. Ask what you can do for your country!"
|
||||
self.client("assets/jfk.flac")
|
||||
with open("output.srt", "r") as f:
|
||||
lines = f.readlines()
|
||||
prediction = " ".join([line.strip() for line in lines[2::4]])
|
||||
prediction_normalized = self.normalizer(prediction)
|
||||
gt_normalized = self.normalizer(gt)
|
||||
|
||||
# calculate WER
|
||||
wer = self.metric.compute(
|
||||
predictions=[prediction_normalized],
|
||||
references=[gt_normalized]
|
||||
)
|
||||
self.assertLess(wer, 0.05)
|
||||
|
||||
|
||||
class TestExceptionHandling(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.server = TranscriptionServer()
|
||||
|
||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||
def test_connection_closed_exception(self, mock_websocket):
|
||||
mock_websocket.recv.side_effect = ConnectionClosed(1001, "testing connection closed")
|
||||
|
||||
with self.assertLogs(level="INFO") as log:
|
||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||
self.assertTrue(any("Connection closed by client" in message for message in log.output))
|
||||
|
||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||
def test_json_decode_exception(self, mock_websocket):
|
||||
mock_websocket.recv.return_value = "invalid json"
|
||||
|
||||
with self.assertLogs(level="ERROR") as log:
|
||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||
self.assertTrue(any("Failed to decode JSON from client" in message for message in log.output))
|
||||
|
||||
@mock.patch('websockets.WebSocketCommonProtocol')
|
||||
def test_unexpected_exception_handling(self, mock_websocket):
|
||||
mock_websocket.recv.side_effect = RuntimeError("Unexpected error")
|
||||
|
||||
with self.assertLogs(level="ERROR") as log:
|
||||
self.server.recv_audio(mock_websocket, "faster_whisper")
|
||||
for message in log.output:
|
||||
print(message)
|
||||
print()
|
||||
self.assertTrue(any("Unexpected error" in message for message in log.output))
|
||||
@@ -0,0 +1,26 @@
|
||||
import unittest
|
||||
import numpy as np
|
||||
from whisper_live.tensorrt_utils import load_audio
|
||||
from whisper_live.vad import VoiceActivityDetector
|
||||
|
||||
|
||||
class TestVoiceActivityDetection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.vad = VoiceActivityDetector()
|
||||
self.sample_rate = 16000
|
||||
|
||||
def generate_silence(self, duration_seconds):
|
||||
return np.zeros(int(self.sample_rate * duration_seconds), dtype=np.float32)
|
||||
|
||||
def load_speech_segment(self, filepath):
|
||||
return load_audio(filepath)
|
||||
|
||||
def test_vad_silence_detection(self):
|
||||
silence = self.generate_silence(3)
|
||||
is_speech_present = self.vad(silence.copy())
|
||||
self.assertFalse(is_speech_present, "VAD incorrectly identified silence as speech.")
|
||||
|
||||
def test_vad_speech_detection(self):
|
||||
audio_tensor = load_audio("assets/jfk.flac")
|
||||
is_speech_present = self.vad(audio_tensor)
|
||||
self.assertTrue(is_speech_present, "VAD failed to identify speech segment.")
|
||||
@@ -1 +1 @@
|
||||
__version__="0.0.7"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
+150
-103
@@ -2,45 +2,14 @@ import os
|
||||
import wave
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
import ffmpeg
|
||||
import pyaudio
|
||||
import threading
|
||||
import textwrap
|
||||
import json
|
||||
import websocket
|
||||
import uuid
|
||||
import time
|
||||
|
||||
|
||||
def resample(file: str, sr: int = 16000):
|
||||
"""
|
||||
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
|
||||
Open an audio file and read as mono waveform, resampling as necessary,
|
||||
save the resampled audio
|
||||
|
||||
Args:
|
||||
file (str): The audio file to open
|
||||
sr (int): The sample rate to resample the audio if necessary
|
||||
|
||||
Returns:
|
||||
resampled_file (str): The resampled audio file
|
||||
"""
|
||||
try:
|
||||
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
||||
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
||||
out, _ = (
|
||||
ffmpeg.input(file, threads=0)
|
||||
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
|
||||
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
||||
)
|
||||
except ffmpeg.Error as e:
|
||||
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
|
||||
np_buffer = np.frombuffer(out, dtype=np.int16)
|
||||
|
||||
resampled_file = f"{file.split('.')[0]}_resampled.wav"
|
||||
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
|
||||
return resampled_file
|
||||
import ffmpeg
|
||||
import whisper_live.utils as utils
|
||||
|
||||
|
||||
class Client:
|
||||
@@ -48,9 +17,17 @@ class Client:
|
||||
Handles audio recording, streaming, and communication with a server using WebSocket.
|
||||
"""
|
||||
INSTANCES = {}
|
||||
END_OF_AUDIO = "END_OF_AUDIO"
|
||||
|
||||
def __init__(
|
||||
self, host=None, port=None, is_multilingual=False, lang=None, translate=False
|
||||
self,
|
||||
host=None,
|
||||
port=None,
|
||||
lang=None,
|
||||
translate=False,
|
||||
model="small",
|
||||
srt_file_path="output.srt",
|
||||
use_vad=True
|
||||
):
|
||||
"""
|
||||
Initializes a Client instance for audio recording and streaming to a server.
|
||||
@@ -62,25 +39,27 @@ class Client:
|
||||
Args:
|
||||
host (str): The hostname or IP address of the server.
|
||||
port (int): The port number for the WebSocket server.
|
||||
is_multilingual (bool, optional): Specifies if multilingual transcription is enabled. Default is False.
|
||||
lang (str, optional): The selected language for transcription when multilingual is disabled. Default is None.
|
||||
lang (str, optional): The selected language for transcription. Default is None.
|
||||
translate (bool, optional): Specifies if the task is translation. Default is False.
|
||||
"""
|
||||
self.chunk = 1024
|
||||
self.chunk = 4096
|
||||
self.format = pyaudio.paInt16
|
||||
self.channels = 1
|
||||
self.rate = 16000
|
||||
self.record_seconds = 60000
|
||||
self.recording = False
|
||||
self.multilingual = False
|
||||
self.language = None
|
||||
self.task = "transcribe"
|
||||
self.uid = str(uuid.uuid4())
|
||||
self.waiting = False
|
||||
self.last_response_recieved = None
|
||||
self.disconnect_if_no_response_for = 15
|
||||
self.multilingual = is_multilingual
|
||||
self.language = lang if is_multilingual else "en"
|
||||
self.language = lang
|
||||
self.model = model
|
||||
self.server_error = False
|
||||
self.srt_file_path = srt_file_path
|
||||
self.use_vad = use_vad
|
||||
self.last_recieved_segment = None
|
||||
|
||||
if translate:
|
||||
self.task = "translate"
|
||||
|
||||
@@ -118,12 +97,47 @@ class Client:
|
||||
self.ws_thread.start()
|
||||
|
||||
self.frames = b""
|
||||
self.transcript = []
|
||||
print("[INFO]: * recording")
|
||||
|
||||
def handle_status_messages(self, message_data):
|
||||
"""Handles server status messages."""
|
||||
status = message_data["status"]
|
||||
if status == "WAIT":
|
||||
self.waiting = True
|
||||
print(f"[INFO]: Server is full. Estimated wait time {round(message_data['message'])} minutes.")
|
||||
elif status == "ERROR":
|
||||
print(f"Message from Server: {message_data['message']}")
|
||||
self.server_error = True
|
||||
elif status == "WARNING":
|
||||
print(f"Message from Server: {message_data['message']}")
|
||||
|
||||
def process_segments(self, segments):
|
||||
"""Processes transcript segments."""
|
||||
text = []
|
||||
for i, seg in enumerate(segments):
|
||||
if not text or text[-1] != seg["text"]:
|
||||
text.append(seg["text"])
|
||||
if i == len(segments) - 1:
|
||||
self.last_segment = seg
|
||||
elif (self.server_backend == "faster_whisper" and
|
||||
(not self.transcript or
|
||||
float(seg['start']) >= float(self.transcript[-1]['end']))):
|
||||
self.transcript.append(seg)
|
||||
# update last received segment and last valild responsne time
|
||||
if self.last_recieved_segment is None or self.last_recieved_segment != segments[-1]["text"]:
|
||||
self.last_response_recieved = time.time()
|
||||
self.last_recieved_segment = segments[-1]["text"]
|
||||
|
||||
# Truncate to last 3 entries for brevity.
|
||||
text = text[-3:]
|
||||
utils.clear_screen()
|
||||
utils.print_transcript(text)
|
||||
|
||||
def on_message(self, ws, message):
|
||||
"""
|
||||
Callback function called when a message is received from the server.
|
||||
|
||||
|
||||
It updates various attributes of the client based on the received message, including
|
||||
recording status, language detection, and server messages. If a disconnect message
|
||||
is received, it sets the recording status to False.
|
||||
@@ -133,25 +147,25 @@ class Client:
|
||||
message (str): The received message from the server.
|
||||
|
||||
"""
|
||||
self.last_response_recieved = time.time()
|
||||
message = json.loads(message)
|
||||
|
||||
if self.uid != message.get("uid"):
|
||||
print("[ERROR]: invalid client uid")
|
||||
return
|
||||
|
||||
if "status" in message.keys() and message["status"] == "WAIT":
|
||||
self.waiting = True
|
||||
print(
|
||||
f"[INFO]:Server is full. Estimated wait time {round(message['message'])} minutes."
|
||||
)
|
||||
if "status" in message.keys():
|
||||
self.handle_status_messages(message)
|
||||
return
|
||||
|
||||
if "message" in message.keys() and message["message"] == "DISCONNECT":
|
||||
print("[INFO]: Server overtime disconnected.")
|
||||
print("[INFO]: Server disconnected due to overtime.")
|
||||
self.recording = False
|
||||
|
||||
if "message" in message.keys() and message["message"] == "SERVER_READY":
|
||||
self.last_response_recieved = time.time()
|
||||
self.recording = True
|
||||
self.server_backend = message["backend"]
|
||||
print(f"[INFO]: Server Running with backend {self.server_backend}")
|
||||
return
|
||||
|
||||
if "language" in message.keys():
|
||||
@@ -162,57 +176,40 @@ class Client:
|
||||
)
|
||||
return
|
||||
|
||||
if "segments" not in message.keys():
|
||||
return
|
||||
|
||||
message = message["segments"]
|
||||
text = []
|
||||
if len(message):
|
||||
for seg in message:
|
||||
if text and text[-1] == seg["text"]:
|
||||
# already got it
|
||||
continue
|
||||
text.append(seg["text"])
|
||||
# keep only last 3
|
||||
if len(text) > 3:
|
||||
text = text[-3:]
|
||||
wrapper = textwrap.TextWrapper(width=60)
|
||||
word_list = wrapper.wrap(text="".join(text))
|
||||
# Print each line.
|
||||
if os.name == "nt":
|
||||
os.system("cls")
|
||||
else:
|
||||
os.system("clear")
|
||||
for element in word_list:
|
||||
print(element)
|
||||
if "segments" in message.keys():
|
||||
self.process_segments(message["segments"])
|
||||
|
||||
def on_error(self, ws, error):
|
||||
print(error)
|
||||
print(f"[ERROR] WebSocket Error: {error}")
|
||||
self.server_error = True
|
||||
self.error_message = error
|
||||
|
||||
def on_close(self, ws, close_status_code, close_msg):
|
||||
print(f"[INFO]: Websocket connection closed: {close_status_code}: {close_msg}")
|
||||
self.recording = False
|
||||
self.server_error = False
|
||||
self.waiting = False
|
||||
|
||||
def on_open(self, ws):
|
||||
"""
|
||||
Callback function called when the WebSocket connection is successfully opened.
|
||||
|
||||
Sends an initial configuration message to the server, including client UID, multilingual mode,
|
||||
|
||||
Sends an initial configuration message to the server, including client UID,
|
||||
language selection, and task type.
|
||||
|
||||
Args:
|
||||
ws (websocket.WebSocketApp): The WebSocket client instance.
|
||||
|
||||
"""
|
||||
print(self.multilingual, self.language, self.task)
|
||||
|
||||
print("[INFO]: Opened connection")
|
||||
ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": self.uid,
|
||||
"multilingual": self.multilingual,
|
||||
"language": self.language,
|
||||
"task": self.task,
|
||||
"model": self.model,
|
||||
"use_vad": self.use_vad
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -221,8 +218,8 @@ class Client:
|
||||
def bytes_to_float_array(audio_bytes):
|
||||
"""
|
||||
Convert audio data from bytes to a NumPy float array.
|
||||
|
||||
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
|
||||
|
||||
It assumes that the audio data is in 16-bit PCM format. The audio data is normalized to
|
||||
have values between -1 and 1.
|
||||
|
||||
Args:
|
||||
@@ -250,10 +247,10 @@ class Client:
|
||||
def play_file(self, filename):
|
||||
"""
|
||||
Play an audio file and send it to the server for processing.
|
||||
|
||||
|
||||
Reads an audio file, plays it through the audio output, and simultaneously sends
|
||||
the audio data to the server for processing. It uses PyAudio to create an audio
|
||||
stream for playback. The audio data is read from the file in chunks, converted to
|
||||
the audio data to the server for processing. It uses PyAudio to create an audio
|
||||
stream for playback. The audio data is read from the file in chunks, converted to
|
||||
floating-point format, and sent to the server using WebSocket communication.
|
||||
This method is typically used when you want to process pre-recorded audio and send it
|
||||
to the server in real-time.
|
||||
@@ -261,7 +258,7 @@ class Client:
|
||||
Args:
|
||||
filename (str): The path to the audio file to be played and sent to the server.
|
||||
"""
|
||||
|
||||
|
||||
# read audio and create pyaudio stream
|
||||
with wave.open(filename, "rb") as wavfile:
|
||||
self.stream = self.p.open(
|
||||
@@ -287,6 +284,9 @@ class Client:
|
||||
assert self.last_response_recieved
|
||||
while time.time() - self.last_response_recieved < self.disconnect_if_no_response_for:
|
||||
continue
|
||||
self.send_packet_to_server(Client.END_OF_AUDIO.encode('utf-8'))
|
||||
if self.server_backend == "faster_whisper":
|
||||
self.write_srt_file(self.srt_file_path)
|
||||
self.stream.close()
|
||||
self.close_websocket()
|
||||
|
||||
@@ -296,13 +296,15 @@ class Client:
|
||||
self.stream.close()
|
||||
self.p.terminate()
|
||||
self.close_websocket()
|
||||
if self.server_backend == "faster_whisper":
|
||||
self.write_srt_file(self.srt_file_path)
|
||||
print("[INFO]: Keyboard interrupt.")
|
||||
|
||||
def close_websocket(self):
|
||||
"""
|
||||
Close the WebSocket connection and join the WebSocket thread.
|
||||
|
||||
First attempts to close the WebSocket connection using `self.client_socket.close()`. After
|
||||
First attempts to close the WebSocket connection using `self.client_socket.close()`. After
|
||||
closing the connection, it joins the WebSocket thread to ensure proper termination.
|
||||
|
||||
"""
|
||||
@@ -329,7 +331,7 @@ class Client:
|
||||
"""
|
||||
Write audio frames to a WAV file.
|
||||
|
||||
The WAV file is created or overwritten with the specified name. The audio frames should be
|
||||
The WAV file is created or overwritten with the specified name. The audio frames should be
|
||||
in the correct format and match the specified channel, sample width, and sample rate.
|
||||
|
||||
Args:
|
||||
@@ -344,6 +346,41 @@ class Client:
|
||||
wavfile.setframerate(self.rate)
|
||||
wavfile.writeframes(frames)
|
||||
|
||||
def process_hls_stream(self, hls_url):
|
||||
"""
|
||||
Connect to an HLS source, process the audio stream, and send it for transcription.
|
||||
|
||||
Args:
|
||||
hls_url (str): The URL of the HLS stream source.
|
||||
"""
|
||||
print("[INFO]: Connecting to HLS stream...")
|
||||
process = None # Initialize process to None
|
||||
|
||||
try:
|
||||
# Connecting to the HLS stream using ffmpeg-python
|
||||
process = (
|
||||
ffmpeg
|
||||
.input(hls_url, threads=0)
|
||||
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar=self.rate)
|
||||
.run_async(pipe_stdout=True, pipe_stderr=True)
|
||||
)
|
||||
|
||||
# Process the stream
|
||||
while True:
|
||||
in_bytes = process.stdout.read(self.chunk * 2) # 2 bytes per sample
|
||||
if not in_bytes:
|
||||
break
|
||||
audio_array = self.bytes_to_float_array(in_bytes)
|
||||
self.send_packet_to_server(audio_array.tobytes())
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR]: Failed to connect to HLS stream: {e}")
|
||||
finally:
|
||||
if process:
|
||||
process.kill()
|
||||
|
||||
print("[INFO]: HLS stream processing finished.")
|
||||
|
||||
def record(self, out_file="output_recording.wav"):
|
||||
"""
|
||||
Record audio data from the input stream and save it to a WAV file.
|
||||
@@ -354,11 +391,12 @@ class Client:
|
||||
|
||||
Audio data is saved in chunks to the "chunks" directory. Each chunk is saved as a separate WAV file.
|
||||
The recording will continue until the specified duration is reached or until the `RECORDING` flag is set to `False`.
|
||||
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
|
||||
The recording process can be interrupted by sending a KeyboardInterrupt (e.g., pressing Ctrl+C). After recording,
|
||||
the method combines all the saved audio chunks into the specified `out_file`.
|
||||
|
||||
Args:
|
||||
out_file (str, optional): The name of the output WAV file to save the entire recording. Default is "output_recording.wav".
|
||||
out_file (str, optional): The name of the output WAV file to save the entire recording.
|
||||
Default is "output_recording.wav".
|
||||
|
||||
"""
|
||||
n_audio_file = 0
|
||||
@@ -368,7 +406,7 @@ class Client:
|
||||
for _ in range(0, int(self.rate / self.chunk * self.record_seconds)):
|
||||
if not self.recording:
|
||||
break
|
||||
data = self.stream.read(self.chunk)
|
||||
data = self.stream.read(self.chunk, exception_on_overflow=False)
|
||||
self.frames += data
|
||||
|
||||
audio_array = Client.bytes_to_float_array(data)
|
||||
@@ -387,6 +425,8 @@ class Client:
|
||||
t.start()
|
||||
n_audio_file += 1
|
||||
self.frames = b""
|
||||
if self.server_backend == "faster_whisper":
|
||||
self.write_srt_file(self.srt_file_path)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if len(self.frames):
|
||||
@@ -400,12 +440,14 @@ class Client:
|
||||
self.close_websocket()
|
||||
|
||||
self.write_output_recording(n_audio_file, out_file)
|
||||
if self.server_backend == "faster_whisper":
|
||||
self.write_srt_file(self.srt_file_path)
|
||||
|
||||
def write_output_recording(self, n_audio_file, out_file):
|
||||
"""
|
||||
Combine and save recorded audio chunks into a single WAV file.
|
||||
|
||||
The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk
|
||||
|
||||
The individual audio chunk files are expected to be located in the "chunks" directory. Reads each chunk
|
||||
file, appends its audio data to the final recording, and then deletes the chunk file. After combining
|
||||
and saving, the final recording is stored in the specified `out_file`.
|
||||
|
||||
@@ -436,6 +478,10 @@ class Client:
|
||||
os.remove(in_file)
|
||||
wavfile.close()
|
||||
|
||||
def write_srt_file(self, output_path="output.srt"):
|
||||
self.transcript.append(self.last_segment)
|
||||
utils.create_srt_file(self.transcript, output_path)
|
||||
|
||||
|
||||
class TranscriptionClient:
|
||||
"""
|
||||
@@ -447,8 +493,7 @@ class TranscriptionClient:
|
||||
Args:
|
||||
host (str): The hostname or IP address of the server.
|
||||
port (int): The port number to connect to on the server.
|
||||
is_multilingual (bool, optional): Indicates whether the transcription should support multiple languages (default is False).
|
||||
lang (str, optional): The primary language for transcription (used if `is_multilingual` is False). Default is None, which defaults to English ('en').
|
||||
lang (str, optional): The primary language for transcription. Default is None, which defaults to English ('en').
|
||||
translate (bool, optional): Indicates whether translation tasks are required (default is False).
|
||||
|
||||
Attributes:
|
||||
@@ -457,34 +502,36 @@ class TranscriptionClient:
|
||||
Example:
|
||||
To create a TranscriptionClient and start transcription on microphone audio:
|
||||
```python
|
||||
transcription_client = TranscriptionClient(host="localhost", port=9090, is_multilingual=True)
|
||||
transcription_client = TranscriptionClient(host="localhost", port=9090)
|
||||
transcription_client()
|
||||
```
|
||||
"""
|
||||
def __init__(self, host, port, is_multilingual=False, lang=None, translate=False):
|
||||
self.client = Client(host, port, is_multilingual, lang, translate)
|
||||
def __init__(self, host, port, lang=None, translate=False, model="small", use_vad=True):
|
||||
self.client = Client(host, port, lang, translate, model, srt_file_path="output.srt", use_vad=use_vad)
|
||||
|
||||
def __call__(self, audio=None):
|
||||
def __call__(self, audio=None, hls_url=None):
|
||||
"""
|
||||
Start the transcription process.
|
||||
|
||||
Initiates the transcription process by connecting to the server via a WebSocket. It waits for the server
|
||||
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it
|
||||
to be ready to receive audio data and then sends audio for transcription. If an audio file is provided, it
|
||||
will be played and streamed to the server; otherwise, it will perform live recording.
|
||||
|
||||
Args:
|
||||
audio (str, optional): Path to an audio file for transcription. Default is None, which triggers live recording.
|
||||
|
||||
|
||||
"""
|
||||
print("[INFO]: Waiting for server ready ...")
|
||||
while not self.client.recording:
|
||||
if self.client.waiting:
|
||||
if self.client.waiting or self.client.server_error:
|
||||
self.client.close_websocket()
|
||||
return
|
||||
pass
|
||||
|
||||
print("[INFO]: Server Ready!")
|
||||
if audio is not None:
|
||||
resampled_file = resample(audio)
|
||||
if hls_url is not None:
|
||||
self.client.process_hls_stream(hls_url)
|
||||
elif audio is not None:
|
||||
resampled_file = utils.resample(audio)
|
||||
self.client.play_file(resampled_file)
|
||||
else:
|
||||
self.client.record()
|
||||
|
||||
+804
-354
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
from typing import Dict, Iterable, List, Optional, TextIO, Tuple, Union
|
||||
|
||||
import kaldialign
|
||||
import numpy as np
|
||||
import soundfile
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
Pathlike = Union[str, Path]
|
||||
|
||||
SAMPLE_RATE = 16000
|
||||
N_FFT = 400
|
||||
HOP_LENGTH = 160
|
||||
CHUNK_LENGTH = 30
|
||||
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
|
||||
|
||||
|
||||
def load_audio(file: str, sr: int = SAMPLE_RATE):
|
||||
"""
|
||||
Open an audio file and read as mono waveform, resampling as necessary
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file: str
|
||||
The audio file to open
|
||||
|
||||
sr: int
|
||||
The sample rate to resample the audio if necessary
|
||||
|
||||
Returns
|
||||
-------
|
||||
A NumPy array containing the audio waveform, in float32 dtype.
|
||||
"""
|
||||
|
||||
# This launches a subprocess to decode audio while down-mixing
|
||||
# and resampling as necessary. Requires the ffmpeg CLI in PATH.
|
||||
# fmt: off
|
||||
cmd = [
|
||||
"ffmpeg", "-nostdin", "-threads", "0", "-i", file, "-f", "s16le", "-ac",
|
||||
"1", "-acodec", "pcm_s16le", "-ar",
|
||||
str(sr), "-"
|
||||
]
|
||||
# fmt: on
|
||||
try:
|
||||
out = run(cmd, capture_output=True, check=True).stdout
|
||||
except CalledProcessError as e:
|
||||
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
|
||||
|
||||
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
||||
|
||||
|
||||
def load_audio_wav_format(wav_path):
|
||||
# make sure audio in .wav format
|
||||
assert wav_path.endswith(
|
||||
'.wav'), f"Only support .wav format, but got {wav_path}"
|
||||
waveform, sample_rate = soundfile.read(wav_path)
|
||||
assert sample_rate == 16000, f"Only support 16k sample rate, but got {sample_rate}"
|
||||
return waveform, sample_rate
|
||||
|
||||
|
||||
def pad_or_trim(array, length: int = N_SAMPLES, *, axis: int = -1):
|
||||
"""
|
||||
Pad or trim the audio array to N_SAMPLES, as expected by the encoder.
|
||||
"""
|
||||
if torch.is_tensor(array):
|
||||
if array.shape[axis] > length:
|
||||
array = array.index_select(dim=axis,
|
||||
index=torch.arange(length,
|
||||
device=array.device))
|
||||
|
||||
if array.shape[axis] < length:
|
||||
pad_widths = [(0, 0)] * array.ndim
|
||||
pad_widths[axis] = (0, length - array.shape[axis])
|
||||
array = F.pad(array,
|
||||
[pad for sizes in pad_widths[::-1] for pad in sizes])
|
||||
else:
|
||||
if array.shape[axis] > length:
|
||||
array = array.take(indices=range(length), axis=axis)
|
||||
|
||||
if array.shape[axis] < length:
|
||||
pad_widths = [(0, 0)] * array.ndim
|
||||
pad_widths[axis] = (0, length - array.shape[axis])
|
||||
array = np.pad(array, pad_widths)
|
||||
|
||||
return array
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def mel_filters(device,
|
||||
n_mels: int,
|
||||
mel_filters_dir: str = None) -> torch.Tensor:
|
||||
"""
|
||||
load the mel filterbank matrix for projecting STFT into a Mel spectrogram.
|
||||
Allows decoupling librosa dependency; saved using:
|
||||
|
||||
np.savez_compressed(
|
||||
"mel_filters.npz",
|
||||
mel_80=librosa.filters.mel(sr=16000, n_fft=400, n_mels=80),
|
||||
)
|
||||
"""
|
||||
assert n_mels in {80, 128}, f"Unsupported n_mels: {n_mels}"
|
||||
if mel_filters_dir is None:
|
||||
mel_filters_path = os.path.join(os.path.dirname(__file__), "assets",
|
||||
"mel_filters.npz")
|
||||
else:
|
||||
mel_filters_path = os.path.join(mel_filters_dir, "mel_filters.npz")
|
||||
with np.load(mel_filters_path) as f:
|
||||
return torch.from_numpy(f[f"mel_{n_mels}"]).to(device)
|
||||
|
||||
|
||||
def log_mel_spectrogram(
|
||||
audio: Union[str, np.ndarray, torch.Tensor],
|
||||
n_mels: int,
|
||||
padding: int = 0,
|
||||
device: Optional[Union[str, torch.device]] = None,
|
||||
return_duration: bool = False,
|
||||
mel_filters_dir: str = None,
|
||||
):
|
||||
"""
|
||||
Compute the log-Mel spectrogram of
|
||||
|
||||
Parameters
|
||||
----------
|
||||
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
|
||||
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
|
||||
|
||||
n_mels: int
|
||||
The number of Mel-frequency filters, only 80 and 128 are supported
|
||||
|
||||
padding: int
|
||||
Number of zero samples to pad to the right
|
||||
|
||||
device: Optional[Union[str, torch.device]]
|
||||
If given, the audio tensor is moved to this device before STFT
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor, shape = (80 or 128, n_frames)
|
||||
A Tensor that contains the Mel spectrogram
|
||||
"""
|
||||
if not torch.is_tensor(audio):
|
||||
if isinstance(audio, str):
|
||||
if audio.endswith('.wav'):
|
||||
audio, _ = load_audio_wav_format(audio)
|
||||
else:
|
||||
audio = load_audio(audio)
|
||||
assert isinstance(audio,
|
||||
np.ndarray), f"Unsupported audio type: {type(audio)}"
|
||||
duration = audio.shape[-1] / SAMPLE_RATE
|
||||
audio = pad_or_trim(audio, N_SAMPLES)
|
||||
audio = audio.astype(np.float32)
|
||||
audio = torch.from_numpy(audio)
|
||||
|
||||
if device is not None:
|
||||
audio = audio.to(device)
|
||||
if padding > 0:
|
||||
audio = F.pad(audio, (0, padding))
|
||||
window = torch.hann_window(N_FFT).to(audio.device)
|
||||
stft = torch.stft(audio,
|
||||
N_FFT,
|
||||
HOP_LENGTH,
|
||||
window=window,
|
||||
return_complex=True)
|
||||
magnitudes = stft[..., :-1].abs()**2
|
||||
|
||||
filters = mel_filters(audio.device, n_mels, mel_filters_dir)
|
||||
mel_spec = filters @ magnitudes
|
||||
|
||||
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
|
||||
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
|
||||
log_spec = (log_spec + 4.0) / 4.0
|
||||
if return_duration:
|
||||
return log_spec, duration
|
||||
else:
|
||||
return log_spec
|
||||
|
||||
|
||||
def store_transcripts(filename: Pathlike, texts: Iterable[Tuple[str, str,
|
||||
str]]) -> None:
|
||||
"""Save predicted results and reference transcripts to a file.
|
||||
https://github.com/k2-fsa/icefall/blob/master/icefall/utils.py
|
||||
Args:
|
||||
filename:
|
||||
File to save the results to.
|
||||
texts:
|
||||
An iterable of tuples. The first element is the cur_id, the second is
|
||||
the reference transcript and the third element is the predicted result.
|
||||
Returns:
|
||||
Return None.
|
||||
"""
|
||||
with open(filename, "w") as f:
|
||||
for cut_id, ref, hyp in texts:
|
||||
print(f"{cut_id}:\tref={ref}", file=f)
|
||||
print(f"{cut_id}:\thyp={hyp}", file=f)
|
||||
|
||||
|
||||
def write_error_stats( # noqa: C901
|
||||
f: TextIO,
|
||||
test_set_name: str,
|
||||
results: List[Tuple[str, str]],
|
||||
enable_log: bool = True,
|
||||
) -> float:
|
||||
"""Write statistics based on predicted results and reference transcripts.
|
||||
https://github.com/k2-fsa/icefall/blob/master/icefall/utils.py
|
||||
It will write the following to the given file:
|
||||
|
||||
- WER
|
||||
- number of insertions, deletions, substitutions, corrects and total
|
||||
reference words. For example::
|
||||
|
||||
Errors: 23 insertions, 57 deletions, 212 substitutions, over 2606
|
||||
reference words (2337 correct)
|
||||
|
||||
- The difference between the reference transcript and predicted result.
|
||||
An instance is given below::
|
||||
|
||||
THE ASSOCIATION OF (EDISON->ADDISON) ILLUMINATING COMPANIES
|
||||
|
||||
The above example shows that the reference word is `EDISON`,
|
||||
but it is predicted to `ADDISON` (a substitution error).
|
||||
|
||||
Another example is::
|
||||
|
||||
FOR THE FIRST DAY (SIR->*) I THINK
|
||||
|
||||
The reference word `SIR` is missing in the predicted
|
||||
results (a deletion error).
|
||||
results:
|
||||
An iterable of tuples. The first element is the cur_id, the second is
|
||||
the reference transcript and the third element is the predicted result.
|
||||
enable_log:
|
||||
If True, also print detailed WER to the console.
|
||||
Otherwise, it is written only to the given file.
|
||||
Returns:
|
||||
Return None.
|
||||
"""
|
||||
subs: Dict[Tuple[str, str], int] = defaultdict(int)
|
||||
ins: Dict[str, int] = defaultdict(int)
|
||||
dels: Dict[str, int] = defaultdict(int)
|
||||
|
||||
# `words` stores counts per word, as follows:
|
||||
# corr, ref_sub, hyp_sub, ins, dels
|
||||
words: Dict[str, List[int]] = defaultdict(lambda: [0, 0, 0, 0, 0])
|
||||
num_corr = 0
|
||||
ERR = "*"
|
||||
for cut_id, ref, hyp in results:
|
||||
ali = kaldialign.align(ref, hyp, ERR)
|
||||
for ref_word, hyp_word in ali:
|
||||
if ref_word == ERR:
|
||||
ins[hyp_word] += 1
|
||||
words[hyp_word][3] += 1
|
||||
elif hyp_word == ERR:
|
||||
dels[ref_word] += 1
|
||||
words[ref_word][4] += 1
|
||||
elif hyp_word != ref_word:
|
||||
subs[(ref_word, hyp_word)] += 1
|
||||
words[ref_word][1] += 1
|
||||
words[hyp_word][2] += 1
|
||||
else:
|
||||
words[ref_word][0] += 1
|
||||
num_corr += 1
|
||||
ref_len = sum([len(r) for _, r, _ in results])
|
||||
sub_errs = sum(subs.values())
|
||||
ins_errs = sum(ins.values())
|
||||
del_errs = sum(dels.values())
|
||||
tot_errs = sub_errs + ins_errs + del_errs
|
||||
tot_err_rate = "%.2f" % (100.0 * tot_errs / ref_len)
|
||||
|
||||
if enable_log:
|
||||
logging.info(f"[{test_set_name}] %WER {tot_errs / ref_len:.2%} "
|
||||
f"[{tot_errs} / {ref_len}, {ins_errs} ins, "
|
||||
f"{del_errs} del, {sub_errs} sub ]")
|
||||
|
||||
print(f"%WER = {tot_err_rate}", file=f)
|
||||
print(
|
||||
f"Errors: {ins_errs} insertions, {del_errs} deletions, "
|
||||
f"{sub_errs} substitutions, over {ref_len} reference "
|
||||
f"words ({num_corr} correct)",
|
||||
file=f,
|
||||
)
|
||||
print(
|
||||
"Search below for sections starting with PER-UTT DETAILS:, "
|
||||
"SUBSTITUTIONS:, DELETIONS:, INSERTIONS:, PER-WORD STATS:",
|
||||
file=f,
|
||||
)
|
||||
|
||||
print("", file=f)
|
||||
print("PER-UTT DETAILS: corr or (ref->hyp) ", file=f)
|
||||
for cut_id, ref, hyp in results:
|
||||
ali = kaldialign.align(ref, hyp, ERR)
|
||||
combine_successive_errors = True
|
||||
if combine_successive_errors:
|
||||
ali = [[[x], [y]] for x, y in ali]
|
||||
for i in range(len(ali) - 1):
|
||||
if ali[i][0] != ali[i][1] and ali[i + 1][0] != ali[i + 1][1]:
|
||||
ali[i + 1][0] = ali[i][0] + ali[i + 1][0]
|
||||
ali[i + 1][1] = ali[i][1] + ali[i + 1][1]
|
||||
ali[i] = [[], []]
|
||||
ali = [[
|
||||
list(filter(lambda a: a != ERR, x)),
|
||||
list(filter(lambda a: a != ERR, y)),
|
||||
] for x, y in ali]
|
||||
ali = list(filter(lambda x: x != [[], []], ali))
|
||||
ali = [[
|
||||
ERR if x == [] else " ".join(x),
|
||||
ERR if y == [] else " ".join(y),
|
||||
] for x, y in ali]
|
||||
|
||||
print(
|
||||
f"{cut_id}:\t" + " ".join((ref_word if ref_word == hyp_word else
|
||||
f"({ref_word}->{hyp_word})"
|
||||
for ref_word, hyp_word in ali)),
|
||||
file=f,
|
||||
)
|
||||
|
||||
print("", file=f)
|
||||
print("SUBSTITUTIONS: count ref -> hyp", file=f)
|
||||
|
||||
for count, (ref, hyp) in sorted([(v, k) for k, v in subs.items()],
|
||||
reverse=True):
|
||||
print(f"{count} {ref} -> {hyp}", file=f)
|
||||
|
||||
print("", file=f)
|
||||
print("DELETIONS: count ref", file=f)
|
||||
for count, ref in sorted([(v, k) for k, v in dels.items()], reverse=True):
|
||||
print(f"{count} {ref}", file=f)
|
||||
|
||||
print("", file=f)
|
||||
print("INSERTIONS: count hyp", file=f)
|
||||
for count, hyp in sorted([(v, k) for k, v in ins.items()], reverse=True):
|
||||
print(f"{count} {hyp}", file=f)
|
||||
|
||||
print("", file=f)
|
||||
print("PER-WORD STATS: word corr tot_errs count_in_ref count_in_hyp",
|
||||
file=f)
|
||||
for _, word, counts in sorted([(sum(v[1:]), k, v)
|
||||
for k, v in words.items()],
|
||||
reverse=True):
|
||||
(corr, ref_sub, hyp_sub, ins, dels) = counts
|
||||
tot_errs = ref_sub + hyp_sub + ins + dels
|
||||
ref_count = corr + ref_sub + dels
|
||||
hyp_count = corr + hyp_sub + ins
|
||||
|
||||
print(f"{word} {corr} {tot_errs} {ref_count} {hyp_count}", file=f)
|
||||
return float(tot_err_rate)
|
||||
+268
-109
@@ -4,7 +4,8 @@ import itertools
|
||||
import logging
|
||||
import os
|
||||
import zlib
|
||||
import logging
|
||||
import json
|
||||
from inspect import signature
|
||||
|
||||
from typing import BinaryIO, Iterable, List, NamedTuple, Optional, Tuple, Union
|
||||
|
||||
@@ -14,21 +15,16 @@ import tokenizers
|
||||
|
||||
from faster_whisper.audio import decode_audio
|
||||
from faster_whisper.feature_extractor import FeatureExtractor
|
||||
from faster_whisper.tokenizer import Tokenizer
|
||||
from faster_whisper.utils import download_model, format_timestamp
|
||||
from faster_whisper.tokenizer import _LANGUAGE_CODES, Tokenizer
|
||||
from faster_whisper.utils import download_model, format_timestamp, get_logger
|
||||
from faster_whisper.vad import (
|
||||
SpeechTimestampsMap,
|
||||
VadOptions,
|
||||
collect_chunks,
|
||||
get_speech_timestamps,
|
||||
)
|
||||
|
||||
|
||||
# implement logger not available in faster_whisper==0.4.1
|
||||
def get_logger():
|
||||
"""Returns the module logger."""
|
||||
return logging.getLogger("faster_whisper")
|
||||
|
||||
|
||||
class Word(NamedTuple):
|
||||
start: float
|
||||
end: float
|
||||
@@ -37,18 +33,17 @@ class Word(NamedTuple):
|
||||
|
||||
|
||||
class Segment(NamedTuple):
|
||||
id: int
|
||||
seek: int
|
||||
start: float
|
||||
end: float
|
||||
text: str
|
||||
words: Optional[List[Word]]
|
||||
avg_log_prob: float
|
||||
tokens: List[int]
|
||||
temperature: float
|
||||
avg_logprob: float
|
||||
compression_ratio: float
|
||||
no_speech_prob: float
|
||||
|
||||
|
||||
class AudioInfo(NamedTuple):
|
||||
language: str
|
||||
language_probability: float
|
||||
duration: float
|
||||
words: Optional[List[Word]]
|
||||
|
||||
|
||||
class TranscriptionOptions(NamedTuple):
|
||||
@@ -56,12 +51,15 @@ class TranscriptionOptions(NamedTuple):
|
||||
best_of: int
|
||||
patience: float
|
||||
length_penalty: float
|
||||
repetition_penalty: float
|
||||
no_repeat_ngram_size: int
|
||||
log_prob_threshold: Optional[float]
|
||||
no_speech_threshold: Optional[float]
|
||||
compression_ratio_threshold: Optional[float]
|
||||
condition_on_previous_text: bool
|
||||
prompt_reset_on_temperature: float
|
||||
temperatures: List[float]
|
||||
initial_prompt: Optional[str]
|
||||
initial_prompt: Optional[Union[str, Iterable[int]]]
|
||||
prefix: Optional[str]
|
||||
suppress_blank: bool
|
||||
suppress_tokens: Optional[List[int]]
|
||||
@@ -72,6 +70,16 @@ class TranscriptionOptions(NamedTuple):
|
||||
append_punctuations: str
|
||||
|
||||
|
||||
class TranscriptionInfo(NamedTuple):
|
||||
language: str
|
||||
language_probability: float
|
||||
duration: float
|
||||
duration_after_vad: float
|
||||
all_language_probs: Optional[List[Tuple[str, float]]]
|
||||
transcription_options: TranscriptionOptions
|
||||
vad_options: VadOptions
|
||||
|
||||
|
||||
class WhisperModel:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -82,14 +90,15 @@ class WhisperModel:
|
||||
cpu_threads: int = 0,
|
||||
num_workers: int = 1,
|
||||
download_root: Optional[str] = None,
|
||||
local_files_only: bool = True,
|
||||
local_files_only: bool = False,
|
||||
):
|
||||
"""Initializes the Whisper model.
|
||||
|
||||
Args:
|
||||
model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en,
|
||||
small, small.en, medium, medium.en, large-v1, or large-v2) or a path to a converted
|
||||
model directory. When a size is configured, the converted model is downloaded
|
||||
small, small.en, medium, medium.en, large-v1, large-v2, large-v3, or large), a path to a converted
|
||||
model directory, or a CTranslate2-converted Whisper model ID from the Hugging Face Hub.
|
||||
When a size or a model ID is configured, the converted model is downloaded
|
||||
from the Hugging Face Hub.
|
||||
device: Device to use for computation ("cpu", "cuda", "auto").
|
||||
device_index: Device ID to use.
|
||||
@@ -104,8 +113,10 @@ class WhisperModel:
|
||||
having multiple workers enables true parallelism when running the model
|
||||
(concurrent calls to self.model.generate() will run in parallel).
|
||||
This can improve the global throughput at the cost of increased memory usage.
|
||||
download_root: Directory where the model should be saved. If not set, the model
|
||||
is saved in the standard Hugging Face cache directory.
|
||||
download_root: Directory where the models should be saved. If not set, the models
|
||||
are saved in the standard Hugging Face cache directory.
|
||||
local_files_only: If True, avoid downloading the file and return the path to the
|
||||
local cached file if it exists.
|
||||
"""
|
||||
self.logger = get_logger()
|
||||
|
||||
@@ -135,7 +146,8 @@ class WhisperModel:
|
||||
"openai/whisper-tiny" + ("" if self.model.is_multilingual else ".en")
|
||||
)
|
||||
|
||||
self.feature_extractor = FeatureExtractor()
|
||||
self.feat_kwargs = self._get_feature_kwargs(model_path)
|
||||
self.feature_extractor = FeatureExtractor(**self.feat_kwargs)
|
||||
self.num_samples_per_token = self.feature_extractor.hop_length * 2
|
||||
self.frames_per_second = (
|
||||
self.feature_extractor.sampling_rate // self.feature_extractor.hop_length
|
||||
@@ -147,6 +159,27 @@ class WhisperModel:
|
||||
self.time_precision = 0.02
|
||||
self.max_length = 448
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> List[str]:
|
||||
"""The languages supported by the model."""
|
||||
return list(_LANGUAGE_CODES) if self.model.is_multilingual else ["en"]
|
||||
|
||||
def _get_feature_kwargs(self, model_path) -> dict:
|
||||
preprocessor_config_file = os.path.join(model_path, "preprocessor_config.json")
|
||||
config = {}
|
||||
if os.path.isfile(preprocessor_config_file):
|
||||
try:
|
||||
with open(preprocessor_config_file, "r", encoding="utf-8") as json_file:
|
||||
config = json.load(json_file)
|
||||
valid_keys = signature(FeatureExtractor.__init__).parameters.keys()
|
||||
config = {k: v for k, v in config.items() if k in valid_keys}
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.warning(
|
||||
"Could not load preprocessor_config.json: %s", str(e)
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
def transcribe(
|
||||
self,
|
||||
audio: Union[str, BinaryIO, np.ndarray],
|
||||
@@ -156,6 +189,8 @@ class WhisperModel:
|
||||
best_of: int = 5,
|
||||
patience: float = 1,
|
||||
length_penalty: float = 1,
|
||||
repetition_penalty: float = 1,
|
||||
no_repeat_ngram_size: int = 0,
|
||||
temperature: Union[float, List[float], Tuple[float, ...]] = [
|
||||
0.0,
|
||||
0.2,
|
||||
@@ -168,7 +203,8 @@ class WhisperModel:
|
||||
log_prob_threshold: Optional[float] = -1.0,
|
||||
no_speech_threshold: Optional[float] = 0.6,
|
||||
condition_on_previous_text: bool = True,
|
||||
initial_prompt: Optional[str] = None,
|
||||
prompt_reset_on_temperature: float = 0.5,
|
||||
initial_prompt: Optional[Union[str, Iterable[int]]] = None,
|
||||
prefix: Optional[str] = None,
|
||||
suppress_blank: bool = True,
|
||||
suppress_tokens: Optional[List[int]] = [-1],
|
||||
@@ -178,8 +214,8 @@ class WhisperModel:
|
||||
prepend_punctuations: str = "\"'“¿([{-",
|
||||
append_punctuations: str = "\"'.。,,!!??::”)]}、",
|
||||
vad_filter: bool = False,
|
||||
vad_parameters: Optional[dict] = None,
|
||||
) -> Tuple[Iterable[Segment], AudioInfo]:
|
||||
vad_parameters: Optional[Union[dict, VadOptions]] = None,
|
||||
) -> Tuple[Iterable[Segment], TranscriptionInfo]:
|
||||
"""Transcribes an input file.
|
||||
|
||||
Arguments:
|
||||
@@ -192,6 +228,9 @@ class WhisperModel:
|
||||
best_of: Number of candidates when sampling with non-zero temperature.
|
||||
patience: Beam search patience factor.
|
||||
length_penalty: Exponential length penalty constant.
|
||||
repetition_penalty: Penalty applied to the score of previously generated tokens
|
||||
(set > 1 to penalize).
|
||||
no_repeat_ngram_size: Prevent repetitions of ngrams with this size (set 0 to disable).
|
||||
temperature: Temperature for sampling. It can be a tuple of temperatures,
|
||||
which will be successively used upon failures according to either
|
||||
`compression_ratio_threshold` or `log_prob_threshold`.
|
||||
@@ -206,7 +245,10 @@ class WhisperModel:
|
||||
as a prompt for the next window; disabling may make the text inconsistent across
|
||||
windows, but the model becomes less prone to getting stuck in a failure loop,
|
||||
such as repetition looping or timestamps going out of sync.
|
||||
initial_prompt: Optional text to provide as a prompt for the first window.
|
||||
prompt_reset_on_temperature: Resets prompt if temperature is above this value.
|
||||
Arg has effect only if condition_on_previous_text is True.
|
||||
initial_prompt: Optional text string or iterable of token ids to provide as a
|
||||
prompt for the first window.
|
||||
prefix: Optional text to provide as a prefix for the first window.
|
||||
suppress_blank: Suppress blank outputs at the beginning of the sampling.
|
||||
suppress_tokens: List of token IDs to suppress. -1 will suppress a default set
|
||||
@@ -222,14 +264,14 @@ class WhisperModel:
|
||||
vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio
|
||||
without speech. This step is using the Silero VAD model
|
||||
https://github.com/snakers4/silero-vad.
|
||||
vad_parameters: Dictionary of Silero VAD parameters (see available parameters and
|
||||
default values in the function `get_speech_timestamps`).
|
||||
vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available
|
||||
parameters and default values in the class `VadOptions`).
|
||||
|
||||
Returns:
|
||||
A tuple with:
|
||||
|
||||
- a generator over transcribed segments
|
||||
- an instance of AudioInfo
|
||||
- an instance of TranscriptionInfo
|
||||
"""
|
||||
sampling_rate = self.feature_extractor.sampling_rate
|
||||
|
||||
@@ -237,19 +279,24 @@ class WhisperModel:
|
||||
audio = decode_audio(audio, sampling_rate=sampling_rate)
|
||||
|
||||
duration = audio.shape[0] / sampling_rate
|
||||
duration_after_vad = duration
|
||||
|
||||
self.logger.info(
|
||||
"Processing audio with duration %s", format_timestamp(duration)
|
||||
)
|
||||
|
||||
if vad_filter:
|
||||
vad_parameters = {} if vad_parameters is None else vad_parameters
|
||||
speech_chunks = get_speech_timestamps(audio, **vad_parameters)
|
||||
if vad_parameters is None:
|
||||
vad_parameters = VadOptions()
|
||||
elif isinstance(vad_parameters, dict):
|
||||
vad_parameters = VadOptions(**vad_parameters)
|
||||
speech_chunks = get_speech_timestamps(audio, vad_parameters)
|
||||
audio = collect_chunks(audio, speech_chunks)
|
||||
duration_after_vad = audio.shape[0] / sampling_rate
|
||||
|
||||
self.logger.info(
|
||||
"VAD filter removed %s of audio",
|
||||
format_timestamp(duration - (audio.shape[0] / sampling_rate)),
|
||||
format_timestamp(duration - duration_after_vad),
|
||||
)
|
||||
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
@@ -271,6 +318,7 @@ class WhisperModel:
|
||||
features = self.feature_extractor(audio)
|
||||
|
||||
encoder_output = None
|
||||
all_language_probs = None
|
||||
|
||||
if language is None:
|
||||
if not self.model.is_multilingual:
|
||||
@@ -279,17 +327,27 @@ class WhisperModel:
|
||||
else:
|
||||
segment = features[:, : self.feature_extractor.nb_max_frames]
|
||||
encoder_output = self.encode(segment)
|
||||
results = self.model.detect_language(encoder_output)
|
||||
language_token, language_probability = results[0][0]
|
||||
language = language_token[2:-2]
|
||||
# results is a list of tuple[str, float] with language names and
|
||||
# probabilities.
|
||||
results = self.model.detect_language(encoder_output)[0]
|
||||
# Parse language names to strip out markers
|
||||
all_language_probs = [(token[2:-2], prob) for (token, prob) in results]
|
||||
# Get top language token and probability
|
||||
language, language_probability = all_language_probs[0]
|
||||
|
||||
self.logger.info(
|
||||
"Detected language '%s' with probability %.2f",
|
||||
language,
|
||||
language_probability,
|
||||
)
|
||||
return language, language_probability
|
||||
else:
|
||||
if not self.model.is_multilingual and language != "en":
|
||||
self.logger.warning(
|
||||
"The current model is English-only but the language parameter is set to '%s'; "
|
||||
"using 'en' instead." % language
|
||||
)
|
||||
language = "en"
|
||||
|
||||
language_probability = 1
|
||||
|
||||
tokenizer = Tokenizer(
|
||||
@@ -304,10 +362,13 @@ class WhisperModel:
|
||||
best_of=best_of,
|
||||
patience=patience,
|
||||
length_penalty=length_penalty,
|
||||
repetition_penalty=repetition_penalty,
|
||||
no_repeat_ngram_size=no_repeat_ngram_size,
|
||||
log_prob_threshold=log_prob_threshold,
|
||||
no_speech_threshold=no_speech_threshold,
|
||||
compression_ratio_threshold=compression_ratio_threshold,
|
||||
condition_on_previous_text=condition_on_previous_text,
|
||||
prompt_reset_on_temperature=prompt_reset_on_temperature,
|
||||
temperatures=(
|
||||
temperature if isinstance(temperature, (list, tuple)) else [temperature]
|
||||
),
|
||||
@@ -327,15 +388,19 @@ class WhisperModel:
|
||||
if speech_chunks:
|
||||
segments = restore_speech_timestamps(segments, speech_chunks, sampling_rate)
|
||||
|
||||
audio_info = AudioInfo(
|
||||
info = TranscriptionInfo(
|
||||
language=language,
|
||||
language_probability=language_probability,
|
||||
duration=duration,
|
||||
duration_after_vad=duration_after_vad,
|
||||
transcription_options=options,
|
||||
vad_options=vad_parameters,
|
||||
all_language_probs=all_language_probs,
|
||||
)
|
||||
|
||||
return segments
|
||||
return segments, info
|
||||
|
||||
def generate_segments(
|
||||
def generate_segments( # noqa: C901
|
||||
self,
|
||||
features: np.ndarray,
|
||||
tokenizer: Tokenizer,
|
||||
@@ -343,18 +408,24 @@ class WhisperModel:
|
||||
encoder_output: Optional[ctranslate2.StorageView] = None,
|
||||
) -> Iterable[Segment]:
|
||||
content_frames = features.shape[-1] - self.feature_extractor.nb_max_frames
|
||||
idx = 0
|
||||
seek = 0
|
||||
all_tokens = []
|
||||
prompt_reset_since = 0
|
||||
|
||||
if options.initial_prompt is not None:
|
||||
initial_prompt = " " + options.initial_prompt.strip()
|
||||
initial_prompt_tokens = tokenizer.encode(initial_prompt)
|
||||
all_tokens.extend(initial_prompt_tokens)
|
||||
if isinstance(options.initial_prompt, str):
|
||||
initial_prompt = " " + options.initial_prompt.strip()
|
||||
initial_prompt_tokens = tokenizer.encode(initial_prompt)
|
||||
all_tokens.extend(initial_prompt_tokens)
|
||||
else:
|
||||
all_tokens.extend(options.initial_prompt)
|
||||
|
||||
last_speech_timestamp = 0.0
|
||||
all_segments = []
|
||||
while seek < content_frames:
|
||||
time_offset = seek * self.feature_extractor.time_per_frame
|
||||
segment = features[:, seek : seek + self.feature_extractor.nb_max_frames]
|
||||
segment = features[:, seek:seek + self.feature_extractor.nb_max_frames]
|
||||
segment_size = min(
|
||||
self.feature_extractor.nb_max_frames, content_frames - seek
|
||||
)
|
||||
@@ -373,12 +444,15 @@ class WhisperModel:
|
||||
prefix=options.prefix if seek == 0 else None,
|
||||
)
|
||||
|
||||
if encoder_output is None:
|
||||
if seek > 0 or encoder_output is None:
|
||||
encoder_output = self.encode(segment)
|
||||
|
||||
result, avg_log_prob, temperature = self.generate_with_fallback(
|
||||
encoder_output, prompt, tokenizer, options
|
||||
)
|
||||
(
|
||||
result,
|
||||
avg_logprob,
|
||||
temperature,
|
||||
compression_ratio,
|
||||
) = self.generate_with_fallback(encoder_output, prompt, tokenizer, options)
|
||||
|
||||
if options.no_speech_threshold is not None:
|
||||
# no voice activity check
|
||||
@@ -386,7 +460,7 @@ class WhisperModel:
|
||||
|
||||
if (
|
||||
options.log_prob_threshold is not None
|
||||
and avg_log_prob > options.log_prob_threshold
|
||||
and avg_logprob > options.log_prob_threshold
|
||||
):
|
||||
# don't skip if the logprob is high enough, despite the no_speech_prob
|
||||
should_skip = False
|
||||
@@ -482,9 +556,6 @@ class WhisperModel:
|
||||
|
||||
seek += segment_size
|
||||
|
||||
if not options.condition_on_previous_text or temperature > 0.5:
|
||||
prompt_reset_since = len(all_tokens)
|
||||
|
||||
if options.word_timestamps:
|
||||
self.add_word_timestamps(
|
||||
current_segments,
|
||||
@@ -493,12 +564,14 @@ class WhisperModel:
|
||||
segment_size,
|
||||
options.prepend_punctuations,
|
||||
options.append_punctuations,
|
||||
last_speech_timestamp=last_speech_timestamp,
|
||||
)
|
||||
|
||||
word_end_timestamps = [
|
||||
w["end"] for s in current_segments for w in s["words"]
|
||||
]
|
||||
|
||||
if len(word_end_timestamps) > 0:
|
||||
last_speech_timestamp = word_end_timestamps[-1]
|
||||
if not single_timestamp_ending and len(word_end_timestamps) > 0:
|
||||
seek_shift = round(
|
||||
(word_end_timestamps[-1] - time_offset) * self.frames_per_second
|
||||
@@ -507,8 +580,6 @@ class WhisperModel:
|
||||
if seek_shift > 0:
|
||||
seek = previous_seek + seek_shift
|
||||
|
||||
encoder_output = None
|
||||
|
||||
for segment in current_segments:
|
||||
tokens = segment["tokens"]
|
||||
text = tokenizer.decode(tokens)
|
||||
@@ -517,19 +588,38 @@ class WhisperModel:
|
||||
continue
|
||||
|
||||
all_tokens.extend(tokens)
|
||||
idx += 1
|
||||
|
||||
all_segments.append(Segment(
|
||||
id=idx,
|
||||
seek=seek,
|
||||
start=segment["start"],
|
||||
end=segment["end"],
|
||||
text=text,
|
||||
tokens=tokens,
|
||||
temperature=temperature,
|
||||
avg_logprob=avg_logprob,
|
||||
compression_ratio=compression_ratio,
|
||||
no_speech_prob=result.no_speech_prob,
|
||||
words=(
|
||||
[Word(**word) for word in segment["words"]]
|
||||
if options.word_timestamps
|
||||
else None
|
||||
),
|
||||
avg_log_prob=avg_log_prob,
|
||||
no_speech_prob=result.no_speech_prob,
|
||||
),
|
||||
))
|
||||
|
||||
if (
|
||||
not options.condition_on_previous_text
|
||||
or temperature > options.prompt_reset_on_temperature
|
||||
):
|
||||
if options.condition_on_previous_text:
|
||||
self.logger.debug(
|
||||
"Reset prompt. prompt_reset_on_temperature threshold is met %f > %f",
|
||||
temperature,
|
||||
options.prompt_reset_on_temperature,
|
||||
)
|
||||
|
||||
prompt_reset_since = len(all_tokens)
|
||||
return all_segments
|
||||
|
||||
def encode(self, features: np.ndarray) -> ctranslate2.StorageView:
|
||||
@@ -548,10 +638,10 @@ class WhisperModel:
|
||||
prompt: List[int],
|
||||
tokenizer: Tokenizer,
|
||||
options: TranscriptionOptions,
|
||||
) -> Tuple[ctranslate2.models.WhisperGenerationResult, float, float]:
|
||||
result = None
|
||||
avg_log_prob = None
|
||||
final_temperature = None
|
||||
) -> Tuple[ctranslate2.models.WhisperGenerationResult, float, float, float]:
|
||||
decode_result = None
|
||||
all_results = []
|
||||
below_cr_threshold_results = []
|
||||
|
||||
max_initial_timestamp_index = int(
|
||||
round(options.max_initial_timestamp / self.time_precision)
|
||||
@@ -571,11 +661,12 @@ class WhisperModel:
|
||||
"patience": options.patience,
|
||||
}
|
||||
|
||||
final_temperature = temperature
|
||||
result = self.model.generate(
|
||||
encoder_output,
|
||||
[prompt],
|
||||
length_penalty=options.length_penalty,
|
||||
repetition_penalty=options.repetition_penalty,
|
||||
no_repeat_ngram_size=options.no_repeat_ngram_size,
|
||||
max_length=self.max_length,
|
||||
return_scores=True,
|
||||
return_no_speech_prob=True,
|
||||
@@ -589,44 +680,63 @@ class WhisperModel:
|
||||
|
||||
# Recover the average log prob from the returned score.
|
||||
seq_len = len(tokens)
|
||||
cum_log_prob = result.scores[0] * (seq_len**options.length_penalty)
|
||||
avg_log_prob = cum_log_prob / (seq_len + 1)
|
||||
cum_logprob = result.scores[0] * (seq_len**options.length_penalty)
|
||||
avg_logprob = cum_logprob / (seq_len + 1)
|
||||
|
||||
text = tokenizer.decode(tokens).strip()
|
||||
compression_ratio = get_compression_ratio(text)
|
||||
|
||||
decode_result = (
|
||||
result,
|
||||
avg_logprob,
|
||||
temperature,
|
||||
compression_ratio,
|
||||
)
|
||||
all_results.append(decode_result)
|
||||
|
||||
needs_fallback = False
|
||||
|
||||
if (
|
||||
options.compression_ratio_threshold is not None
|
||||
and compression_ratio > options.compression_ratio_threshold
|
||||
):
|
||||
needs_fallback = True # too repetitive
|
||||
if options.compression_ratio_threshold is not None:
|
||||
if compression_ratio > options.compression_ratio_threshold:
|
||||
needs_fallback = True # too repetitive
|
||||
|
||||
self.logger.debug(
|
||||
"Compression ratio threshold is not met with temperature %.1f (%f > %f)",
|
||||
temperature,
|
||||
compression_ratio,
|
||||
options.compression_ratio_threshold,
|
||||
)
|
||||
self.logger.debug(
|
||||
"Compression ratio threshold is not met with temperature %.1f (%f > %f)",
|
||||
temperature,
|
||||
compression_ratio,
|
||||
options.compression_ratio_threshold,
|
||||
)
|
||||
else:
|
||||
below_cr_threshold_results.append(decode_result)
|
||||
|
||||
if (
|
||||
options.log_prob_threshold is not None
|
||||
and avg_log_prob < options.log_prob_threshold
|
||||
and avg_logprob < options.log_prob_threshold
|
||||
):
|
||||
needs_fallback = True # average log probability is too low
|
||||
|
||||
self.logger.debug(
|
||||
"Log probability threshold is not met with temperature %.1f (%f < %f)",
|
||||
temperature,
|
||||
avg_log_prob,
|
||||
avg_logprob,
|
||||
options.log_prob_threshold,
|
||||
)
|
||||
|
||||
if (
|
||||
options.no_speech_threshold is not None
|
||||
and result.no_speech_prob > options.no_speech_threshold
|
||||
):
|
||||
needs_fallback = False # silence
|
||||
|
||||
if not needs_fallback:
|
||||
break
|
||||
else:
|
||||
# all failed, select the result with the highest average log probability
|
||||
decode_result = max(
|
||||
below_cr_threshold_results or all_results, key=lambda x: x[1]
|
||||
)
|
||||
|
||||
return result, avg_log_prob, final_temperature
|
||||
return decode_result
|
||||
|
||||
def get_prompt(
|
||||
self,
|
||||
@@ -639,7 +749,7 @@ class WhisperModel:
|
||||
|
||||
if previous_tokens:
|
||||
prompt.append(tokenizer.sot_prev)
|
||||
prompt.extend(previous_tokens[-(self.max_length // 2 - 1) :])
|
||||
prompt.extend(previous_tokens[-(self.max_length // 2 - 1):])
|
||||
|
||||
prompt.extend(tokenizer.sot_sequence)
|
||||
|
||||
@@ -650,11 +760,13 @@ class WhisperModel:
|
||||
prefix_tokens = tokenizer.encode(" " + prefix.strip())
|
||||
if len(prefix_tokens) >= self.max_length // 2:
|
||||
prefix_tokens = prefix_tokens[: self.max_length // 2 - 1]
|
||||
if not without_timestamps:
|
||||
prompt.append(tokenizer.timestamp_begin)
|
||||
prompt.extend(prefix_tokens)
|
||||
|
||||
return prompt
|
||||
|
||||
def add_word_timestamps(
|
||||
def add_word_timestamps( # noqa: C901
|
||||
self,
|
||||
segments: List[dict],
|
||||
tokenizer: Tokenizer,
|
||||
@@ -662,7 +774,8 @@ class WhisperModel:
|
||||
num_frames: int,
|
||||
prepend_punctuations: str,
|
||||
append_punctuations: str,
|
||||
):
|
||||
last_speech_timestamp: float,
|
||||
) -> None:
|
||||
if len(segments) == 0:
|
||||
return
|
||||
|
||||
@@ -675,6 +788,24 @@ class WhisperModel:
|
||||
alignment = self.find_alignment(
|
||||
tokenizer, text_tokens, encoder_output, num_frames
|
||||
)
|
||||
word_durations = np.array([word["end"] - word["start"] for word in alignment])
|
||||
word_durations = word_durations[word_durations.nonzero()]
|
||||
median_duration = np.median(word_durations) if len(word_durations) > 0 else 0.0
|
||||
max_duration = median_duration * 2
|
||||
|
||||
# hack: truncate long words at sentence boundaries.
|
||||
# a better segmentation algorithm based on VAD should be able to replace this.
|
||||
if len(word_durations) > 0:
|
||||
sentence_end_marks = ".。!!??"
|
||||
# ensure words at sentence boundaries
|
||||
# are not longer than twice the median word duration.
|
||||
for i in range(1, len(alignment)):
|
||||
if alignment[i]["end"] - alignment[i]["start"] > max_duration:
|
||||
if alignment[i]["word"] in sentence_end_marks:
|
||||
alignment[i]["end"] = alignment[i]["start"] + max_duration
|
||||
elif alignment[i - 1]["word"] in sentence_end_marks:
|
||||
alignment[i]["start"] = alignment[i]["end"] - max_duration
|
||||
|
||||
merge_punctuations(alignment, prepend_punctuations, append_punctuations)
|
||||
|
||||
time_offset = (
|
||||
@@ -705,10 +836,51 @@ class WhisperModel:
|
||||
saved_tokens += len(timing["tokens"])
|
||||
word_index += 1
|
||||
|
||||
# hack: truncate long words at segment boundaries.
|
||||
# a better segmentation algorithm based on VAD should be able to replace this.
|
||||
if len(words) > 0:
|
||||
# adjust the segment-level timestamps based on the word-level timestamps
|
||||
segment["start"] = words[0]["start"]
|
||||
segment["end"] = words[-1]["end"]
|
||||
# ensure the first and second word after a pause is not longer than
|
||||
# twice the median word duration.
|
||||
if words[0]["end"] - last_speech_timestamp > median_duration * 4 and (
|
||||
words[0]["end"] - words[0]["start"] > max_duration
|
||||
or (
|
||||
len(words) > 1
|
||||
and words[1]["end"] - words[0]["start"] > max_duration * 2
|
||||
)
|
||||
):
|
||||
if (
|
||||
len(words) > 1
|
||||
and words[1]["end"] - words[1]["start"] > max_duration
|
||||
):
|
||||
boundary = max(
|
||||
words[1]["end"] / 2, words[1]["end"] - max_duration
|
||||
)
|
||||
words[0]["end"] = words[1]["start"] = boundary
|
||||
words[0]["start"] = max(0, words[0]["end"] - max_duration)
|
||||
|
||||
# prefer the segment-level start timestamp if the first word is too long.
|
||||
if (
|
||||
segment["start"] < words[0]["end"]
|
||||
and segment["start"] - 0.5 > words[0]["start"]
|
||||
):
|
||||
words[0]["start"] = max(
|
||||
0, min(words[0]["end"] - median_duration, segment["start"])
|
||||
)
|
||||
else:
|
||||
segment["start"] = words[0]["start"]
|
||||
|
||||
# prefer the segment-level end timestamp if the last word is too long.
|
||||
if (
|
||||
segment["end"] > words[-1]["start"]
|
||||
and segment["end"] + 0.5 < words[-1]["end"]
|
||||
):
|
||||
words[-1]["end"] = max(
|
||||
words[-1]["start"] + median_duration, segment["end"]
|
||||
)
|
||||
else:
|
||||
segment["end"] = words[-1]["end"]
|
||||
|
||||
last_speech_timestamp = segment["end"]
|
||||
|
||||
segment["words"] = words
|
||||
|
||||
@@ -741,6 +913,8 @@ class WhisperModel:
|
||||
text_tokens + [tokenizer.eot]
|
||||
)
|
||||
word_boundaries = np.pad(np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0))
|
||||
if len(word_boundaries) <= 1:
|
||||
return []
|
||||
|
||||
jumps = np.pad(np.diff(text_indices), (1, 0), constant_values=1).astype(bool)
|
||||
jump_times = time_indices[jumps] / self.tokens_per_second
|
||||
@@ -751,22 +925,6 @@ class WhisperModel:
|
||||
for i, j in zip(word_boundaries[:-1], word_boundaries[1:])
|
||||
]
|
||||
|
||||
# hack: ensure the first and second word is not longer than twice the median word duration.
|
||||
# a better segmentation algorithm based on VAD should be able to replace this.
|
||||
word_durations = end_times - start_times
|
||||
word_durations = word_durations[word_durations.nonzero()]
|
||||
if len(word_durations) > 0:
|
||||
median_duration = np.median(word_durations)
|
||||
max_duration = median_duration * 2
|
||||
if len(word_durations) >= 2 and word_durations[1] > max_duration:
|
||||
boundary = max(end_times[2] / 2, end_times[2] - max_duration)
|
||||
end_times[0] = start_times[1] = boundary
|
||||
if (
|
||||
len(word_durations) >= 1
|
||||
and end_times[0] - start_times[0] > max_duration
|
||||
):
|
||||
start_times[0] = max(0, end_times[0] - max_duration)
|
||||
|
||||
return [
|
||||
dict(
|
||||
word=word, tokens=tokens, start=start, end=end, probability=probability
|
||||
@@ -775,9 +933,6 @@ class WhisperModel:
|
||||
words, word_tokens, start_times, end_times, word_probabilities
|
||||
)
|
||||
]
|
||||
|
||||
def destroy(self):
|
||||
del self.model
|
||||
|
||||
|
||||
def restore_speech_timestamps(
|
||||
@@ -792,7 +947,8 @@ def restore_speech_timestamps(
|
||||
words = []
|
||||
for word in segment.words:
|
||||
# Ensure the word start and end times are resolved to the same chunk.
|
||||
chunk_index = ts_map.get_chunk_index(word.start)
|
||||
middle = (word.start + word.end) / 2
|
||||
chunk_index = ts_map.get_chunk_index(middle)
|
||||
word = word._replace(
|
||||
start=ts_map.get_original_time(word.start, chunk_index),
|
||||
end=ts_map.get_original_time(word.end, chunk_index),
|
||||
@@ -811,7 +967,7 @@ def restore_speech_timestamps(
|
||||
end=ts_map.get_original_time(segment.end),
|
||||
)
|
||||
|
||||
yield segment
|
||||
return segments
|
||||
|
||||
|
||||
def get_ctranslate2_storage(segment: np.ndarray) -> ctranslate2.StorageView:
|
||||
@@ -825,7 +981,10 @@ def get_compression_ratio(text: str) -> float:
|
||||
return len(text_bytes) / len(zlib.compress(text_bytes))
|
||||
|
||||
|
||||
def get_suppressed_tokens(tokenizer, suppress_tokens):
|
||||
def get_suppressed_tokens(
|
||||
tokenizer: Tokenizer,
|
||||
suppress_tokens: Optional[List[int]],
|
||||
) -> Optional[List[int]]:
|
||||
if not suppress_tokens or -1 in suppress_tokens:
|
||||
return suppress_tokens
|
||||
|
||||
@@ -846,7 +1005,7 @@ def get_suppressed_tokens(tokenizer, suppress_tokens):
|
||||
return sorted(set(suppress_tokens))
|
||||
|
||||
|
||||
def merge_punctuations(alignment: List[dict], prepended: str, appended: str):
|
||||
def merge_punctuations(alignment: List[dict], prepended: str, appended: str) -> None:
|
||||
# merge prepended punctuations
|
||||
i = len(alignment) - 2
|
||||
j = len(alignment) - 1
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import json
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import torch.nn.functional as F
|
||||
from whisper.tokenizer import get_tokenizer
|
||||
from whisper_live.tensorrt_utils import (mel_filters, load_audio_wav_format, pad_or_trim, load_audio)
|
||||
|
||||
import tensorrt_llm
|
||||
import tensorrt_llm.logger as logger
|
||||
from tensorrt_llm._utils import (str_dtype_to_torch, str_dtype_to_trt,
|
||||
trt_dtype_to_torch)
|
||||
from tensorrt_llm.runtime import ModelConfig, SamplingConfig
|
||||
from tensorrt_llm.runtime.session import Session, TensorInfo
|
||||
|
||||
|
||||
SAMPLE_RATE = 16000
|
||||
N_FFT = 400
|
||||
HOP_LENGTH = 160
|
||||
CHUNK_LENGTH = 30
|
||||
N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
|
||||
|
||||
|
||||
class WhisperEncoding:
|
||||
|
||||
def __init__(self, engine_dir):
|
||||
self.session = self.get_session(engine_dir)
|
||||
|
||||
def get_session(self, engine_dir):
|
||||
config_path = engine_dir / 'encoder_config.json'
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
dtype = config['builder_config']['precision']
|
||||
n_mels = config['builder_config']['n_mels']
|
||||
num_languages = config['builder_config']['num_languages']
|
||||
|
||||
self.dtype = dtype
|
||||
self.n_mels = n_mels
|
||||
self.num_languages = num_languages
|
||||
|
||||
serialize_path = engine_dir / f'whisper_encoder_{self.dtype}_tp1_rank0.engine'
|
||||
|
||||
with open(serialize_path, 'rb') as f:
|
||||
session = Session.from_serialized_engine(f.read())
|
||||
|
||||
return session
|
||||
|
||||
def get_audio_features(self, mel):
|
||||
inputs = OrderedDict()
|
||||
output_list = []
|
||||
|
||||
inputs.update({'x': mel})
|
||||
output_list.append(
|
||||
TensorInfo('x', str_dtype_to_trt(self.dtype), mel.shape))
|
||||
|
||||
output_info = (self.session).infer_shapes(output_list)
|
||||
|
||||
logger.debug(f'output info {output_info}')
|
||||
outputs = {
|
||||
t.name: torch.empty(tuple(t.shape),
|
||||
dtype=trt_dtype_to_torch(t.dtype),
|
||||
device='cuda')
|
||||
for t in output_info
|
||||
}
|
||||
stream = torch.cuda.current_stream()
|
||||
ok = self.session.run(inputs=inputs,
|
||||
outputs=outputs,
|
||||
stream=stream.cuda_stream)
|
||||
assert ok, 'Engine execution failed'
|
||||
stream.synchronize()
|
||||
audio_features = outputs['output']
|
||||
return audio_features
|
||||
|
||||
|
||||
class WhisperDecoding:
|
||||
|
||||
def __init__(self, engine_dir, runtime_mapping, debug_mode=False):
|
||||
|
||||
self.decoder_config = self.get_config(engine_dir)
|
||||
self.decoder_generation_session = self.get_session(
|
||||
engine_dir, runtime_mapping, debug_mode)
|
||||
|
||||
def get_config(self, engine_dir):
|
||||
config_path = engine_dir / 'decoder_config.json'
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
decoder_config = OrderedDict()
|
||||
decoder_config.update(config['plugin_config'])
|
||||
decoder_config.update(config['builder_config'])
|
||||
return decoder_config
|
||||
|
||||
def get_session(self, engine_dir, runtime_mapping, debug_mode=False):
|
||||
dtype = self.decoder_config['precision']
|
||||
serialize_path = engine_dir / f'whisper_decoder_{dtype}_tp1_rank0.engine'
|
||||
with open(serialize_path, "rb") as f:
|
||||
decoder_engine_buffer = f.read()
|
||||
|
||||
decoder_model_config = ModelConfig(
|
||||
num_heads=self.decoder_config['num_heads'],
|
||||
num_kv_heads=self.decoder_config['num_heads'],
|
||||
hidden_size=self.decoder_config['hidden_size'],
|
||||
vocab_size=self.decoder_config['vocab_size'],
|
||||
num_layers=self.decoder_config['num_layers'],
|
||||
gpt_attention_plugin=self.decoder_config['gpt_attention_plugin'],
|
||||
remove_input_padding=self.decoder_config['remove_input_padding'],
|
||||
cross_attention=self.decoder_config['cross_attention'],
|
||||
has_position_embedding=self.
|
||||
decoder_config['has_position_embedding'],
|
||||
has_token_type_embedding=self.
|
||||
decoder_config['has_token_type_embedding'],
|
||||
)
|
||||
decoder_generation_session = tensorrt_llm.runtime.GenerationSession(
|
||||
decoder_model_config,
|
||||
decoder_engine_buffer,
|
||||
runtime_mapping,
|
||||
debug_mode=debug_mode)
|
||||
|
||||
return decoder_generation_session
|
||||
|
||||
def generate(self,
|
||||
decoder_input_ids,
|
||||
encoder_outputs,
|
||||
eot_id,
|
||||
max_new_tokens=40,
|
||||
num_beams=1):
|
||||
encoder_input_lengths = torch.tensor(
|
||||
[encoder_outputs.shape[1] for x in range(encoder_outputs.shape[0])],
|
||||
dtype=torch.int32,
|
||||
device='cuda')
|
||||
|
||||
decoder_input_lengths = torch.tensor([
|
||||
decoder_input_ids.shape[-1]
|
||||
for _ in range(decoder_input_ids.shape[0])
|
||||
],
|
||||
dtype=torch.int32,
|
||||
device='cuda')
|
||||
decoder_max_input_length = torch.max(decoder_input_lengths).item()
|
||||
|
||||
# generation config
|
||||
sampling_config = SamplingConfig(end_id=eot_id,
|
||||
pad_id=eot_id,
|
||||
num_beams=num_beams)
|
||||
self.decoder_generation_session.setup(
|
||||
decoder_input_lengths.size(0),
|
||||
decoder_max_input_length,
|
||||
max_new_tokens,
|
||||
beam_width=num_beams,
|
||||
encoder_max_input_length=encoder_outputs.shape[1])
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
decoder_input_ids = decoder_input_ids.type(torch.int32).cuda()
|
||||
output_ids = self.decoder_generation_session.decode(
|
||||
decoder_input_ids,
|
||||
decoder_input_lengths,
|
||||
sampling_config,
|
||||
encoder_output=encoder_outputs,
|
||||
encoder_input_lengths=encoder_input_lengths,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# get the list of int from output_ids tensor
|
||||
output_ids = output_ids.cpu().numpy().tolist()
|
||||
return output_ids
|
||||
|
||||
|
||||
class WhisperTRTLLM(object):
|
||||
|
||||
def __init__(self, engine_dir, assets_dir=None, device=None, is_multilingual=False,
|
||||
language="en", task="transcribe"):
|
||||
world_size = 1
|
||||
runtime_rank = tensorrt_llm.mpi_rank()
|
||||
runtime_mapping = tensorrt_llm.Mapping(world_size, runtime_rank)
|
||||
torch.cuda.set_device(runtime_rank % runtime_mapping.gpus_per_node)
|
||||
engine_dir = Path(engine_dir)
|
||||
|
||||
self.encoder = WhisperEncoding(engine_dir)
|
||||
self.decoder = WhisperDecoding(engine_dir,
|
||||
runtime_mapping,
|
||||
debug_mode=False)
|
||||
self.n_mels = self.encoder.n_mels
|
||||
# self.tokenizer = get_tokenizer(num_languages=self.encoder.num_languages,
|
||||
# tokenizer_dir=assets_dir)
|
||||
self.device = device
|
||||
self.tokenizer = get_tokenizer(
|
||||
is_multilingual,
|
||||
num_languages=self.encoder.num_languages,
|
||||
language=language,
|
||||
task=task,
|
||||
)
|
||||
self.filters = mel_filters(self.device, self.encoder.n_mels, assets_dir)
|
||||
|
||||
def log_mel_spectrogram(
|
||||
self,
|
||||
audio: Union[str, np.ndarray, torch.Tensor],
|
||||
padding: int = 0,
|
||||
return_duration=True
|
||||
):
|
||||
"""
|
||||
Compute the log-Mel spectrogram of
|
||||
|
||||
Parameters
|
||||
----------
|
||||
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
|
||||
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
|
||||
|
||||
n_mels: int
|
||||
The number of Mel-frequency filters, only 80 and 128 are supported
|
||||
|
||||
padding: int
|
||||
Number of zero samples to pad to the right
|
||||
|
||||
device: Optional[Union[str, torch.device]]
|
||||
If given, the audio tensor is moved to this device before STFT
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor, shape = (80 or 128, n_frames)
|
||||
A Tensor that contains the Mel spectrogram
|
||||
"""
|
||||
if not torch.is_tensor(audio):
|
||||
if isinstance(audio, str):
|
||||
if audio.endswith('.wav'):
|
||||
audio, _ = load_audio_wav_format(audio)
|
||||
else:
|
||||
audio = load_audio(audio)
|
||||
assert isinstance(audio, np.ndarray), f"Unsupported audio type: {type(audio)}"
|
||||
duration = audio.shape[-1] / SAMPLE_RATE
|
||||
audio = pad_or_trim(audio, N_SAMPLES)
|
||||
audio = audio.astype(np.float32)
|
||||
audio = torch.from_numpy(audio)
|
||||
|
||||
if self.device is not None:
|
||||
audio = audio.to(self.device)
|
||||
if padding > 0:
|
||||
audio = F.pad(audio, (0, padding))
|
||||
window = torch.hann_window(N_FFT).to(audio.device)
|
||||
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
|
||||
magnitudes = stft[..., :-1].abs()**2
|
||||
|
||||
mel_spec = self.filters @ magnitudes
|
||||
|
||||
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
|
||||
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
|
||||
log_spec = (log_spec + 4.0) / 4.0
|
||||
if return_duration:
|
||||
return log_spec, duration
|
||||
else:
|
||||
return log_spec
|
||||
|
||||
def process_batch(
|
||||
self,
|
||||
mel,
|
||||
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
|
||||
num_beams=1):
|
||||
prompt_id = self.tokenizer.encode(
|
||||
text_prefix, allowed_special=set(self.tokenizer.special_tokens.keys()))
|
||||
|
||||
prompt_id = torch.tensor(prompt_id)
|
||||
batch_size = mel.shape[0]
|
||||
decoder_input_ids = prompt_id.repeat(batch_size, 1)
|
||||
|
||||
encoder_output = self.encoder.get_audio_features(mel)
|
||||
output_ids = self.decoder.generate(decoder_input_ids,
|
||||
encoder_output,
|
||||
self.tokenizer.eot,
|
||||
max_new_tokens=96,
|
||||
num_beams=num_beams)
|
||||
texts = []
|
||||
for i in range(len(output_ids)):
|
||||
text = self.tokenizer.decode(output_ids[i][0]).strip()
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
def transcribe(
|
||||
self,
|
||||
mel,
|
||||
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
|
||||
dtype='float16',
|
||||
batch_size=1,
|
||||
num_beams=1,
|
||||
):
|
||||
mel = mel.type(str_dtype_to_torch(dtype))
|
||||
mel = mel.unsqueeze(0)
|
||||
predictions = self.process_batch(mel, text_prefix, num_beams)
|
||||
prediction = predictions[0]
|
||||
|
||||
# remove all special tokens in the prediction
|
||||
prediction = re.sub(r'<\|.*?\|>', '', prediction)
|
||||
return prediction.strip()
|
||||
|
||||
|
||||
def decode_wav_file(
|
||||
model,
|
||||
mel,
|
||||
text_prefix="<|startoftranscript|><|en|><|transcribe|><|notimestamps|>",
|
||||
dtype='float16',
|
||||
batch_size=1,
|
||||
num_beams=1,
|
||||
normalizer=None,
|
||||
mel_filters_dir=None):
|
||||
|
||||
mel = mel.type(str_dtype_to_torch(dtype))
|
||||
mel = mel.unsqueeze(0)
|
||||
# repeat the mel spectrogram to match the batch size
|
||||
mel = mel.repeat(batch_size, 1, 1)
|
||||
predictions = model.process_batch(mel, text_prefix, num_beams)
|
||||
prediction = predictions[0]
|
||||
|
||||
# remove all special tokens in the prediction
|
||||
prediction = re.sub(r'<\|.*?\|>', '', prediction)
|
||||
if normalizer:
|
||||
prediction = normalizer(prediction)
|
||||
|
||||
return prediction.strip()
|
||||
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import textwrap
|
||||
import scipy
|
||||
import ffmpeg
|
||||
import numpy as np
|
||||
|
||||
|
||||
def clear_screen():
|
||||
"""Clears the console screen."""
|
||||
os.system("cls" if os.name == "nt" else "clear")
|
||||
|
||||
|
||||
def print_transcript(text):
|
||||
"""Prints formatted transcript text."""
|
||||
wrapper = textwrap.TextWrapper(width=60)
|
||||
for line in wrapper.wrap(text="".join(text)):
|
||||
print(line)
|
||||
|
||||
|
||||
def format_time(s):
|
||||
"""Convert seconds (float) to SRT time format."""
|
||||
hours = int(s // 3600)
|
||||
minutes = int((s % 3600) // 60)
|
||||
seconds = int(s % 60)
|
||||
milliseconds = int((s - int(s)) * 1000)
|
||||
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
|
||||
|
||||
|
||||
def create_srt_file(segments, output_file):
|
||||
with open(output_file, 'w', encoding='utf-8') as srt_file:
|
||||
segment_number = 1
|
||||
for segment in segments:
|
||||
start_time = format_time(float(segment['start']))
|
||||
end_time = format_time(float(segment['end']))
|
||||
text = segment['text']
|
||||
|
||||
srt_file.write(f"{segment_number}\n")
|
||||
srt_file.write(f"{start_time} --> {end_time}\n")
|
||||
srt_file.write(f"{text}\n\n")
|
||||
|
||||
segment_number += 1
|
||||
|
||||
|
||||
def resample(file: str, sr: int = 16000):
|
||||
"""
|
||||
# https://github.com/openai/whisper/blob/7858aa9c08d98f75575035ecd6481f462d66ca27/whisper/audio.py#L22
|
||||
Open an audio file and read as mono waveform, resampling as necessary,
|
||||
save the resampled audio
|
||||
|
||||
Args:
|
||||
file (str): The audio file to open
|
||||
sr (int): The sample rate to resample the audio if necessary
|
||||
|
||||
Returns:
|
||||
resampled_file (str): The resampled audio file
|
||||
"""
|
||||
try:
|
||||
# This launches a subprocess to decode audio while down-mixing and resampling as necessary.
|
||||
# Requires the ffmpeg CLI and `ffmpeg-python` package to be installed.
|
||||
out, _ = (
|
||||
ffmpeg.input(file, threads=0)
|
||||
.output("-", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
|
||||
.run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True)
|
||||
)
|
||||
except ffmpeg.Error as e:
|
||||
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
|
||||
np_buffer = np.frombuffer(out, dtype=np.int16)
|
||||
|
||||
resampled_file = f"{file.split('.')[0]}_resampled.wav"
|
||||
scipy.io.wavfile.write(resampled_file, sr, np_buffer.astype(np.int16))
|
||||
return resampled_file
|
||||
+30
-3
@@ -11,6 +11,7 @@ class VoiceActivityDetection():
|
||||
|
||||
def __init__(self, force_onnx_cpu=True):
|
||||
path = self.download()
|
||||
|
||||
opts = onnxruntime.SessionOptions()
|
||||
opts.log_severity_level = 3
|
||||
|
||||
@@ -22,7 +23,6 @@ class VoiceActivityDetection():
|
||||
else:
|
||||
self.session = onnxruntime.InferenceSession(path, providers=['CUDAExecutionProvider'], sess_options=opts)
|
||||
|
||||
|
||||
self.reset_states()
|
||||
self.sample_rates = [8000, 16000]
|
||||
|
||||
@@ -34,7 +34,7 @@ class VoiceActivityDetection():
|
||||
|
||||
if sr != 16000 and (sr % 16000 == 0):
|
||||
step = sr // 16000
|
||||
x = x[:,::step]
|
||||
x = x[:, ::step]
|
||||
sr = 16000
|
||||
|
||||
if sr not in self.sample_rates:
|
||||
@@ -106,10 +106,37 @@ class VoiceActivityDetection():
|
||||
# Check if the model file already exists
|
||||
if not os.path.exists(model_filename):
|
||||
# If it doesn't exist, download the model using wget
|
||||
print("Downloading VAD ONNX model...")
|
||||
try:
|
||||
subprocess.run(["wget", "-O", model_filename, model_url], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Failed to download the model using wget.")
|
||||
return model_filename
|
||||
|
||||
|
||||
class VoiceActivityDetector:
|
||||
def __init__(self, threshold=0.5, frame_rate=16000):
|
||||
"""
|
||||
Initializes the VoiceActivityDetector with a voice activity detection model and a threshold.
|
||||
|
||||
Args:
|
||||
threshold (float, optional): The probability threshold for detecting voice activity. Defaults to 0.5.
|
||||
"""
|
||||
self.model = VoiceActivityDetection()
|
||||
self.threshold = threshold
|
||||
self.frame_rate = frame_rate
|
||||
|
||||
def __call__(self, audio_frame):
|
||||
"""
|
||||
Determines if the given audio frame contains speech by comparing the detected speech probability against
|
||||
the threshold.
|
||||
|
||||
Args:
|
||||
audio_frame (np.ndarray): The audio frame to be analyzed for voice activity. It is expected to be a
|
||||
NumPy array of audio samples.
|
||||
|
||||
Returns:
|
||||
bool: True if the speech probability exceeds the threshold, indicating the presence of voice activity;
|
||||
False otherwise.
|
||||
"""
|
||||
speech_prob = self.model(torch.from_numpy(audio_frame), self.frame_rate).item()
|
||||
return speech_prob > self.threshold
|
||||
|
||||
Reference in New Issue
Block a user