Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sovaai/sova-asr
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: sxdxfan/sova-asr
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 4 commits
  • 148 files changed
  • 1 contributor

Commits on Oct 23, 2021

  1. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    709bd86 View commit details
  2. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    323ae01 View commit details
  3. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    152a1b4 View commit details
  4. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    19b662d View commit details
Showing with 6,112 additions and 180 deletions.
  1. +87 −94 README.md
  2. +7 −3 app.py
  3. +12 −3 config.ini
  4. +2 −3 data_loader.py
  5. +13 −8 decoder.py
  6. +31 −0 decoder_app.py
  7. +27 −5 docker-compose.yml
  8. +103 −14 file_handler.py
  9. +60 −0 frontend/README.md
  10. +6 −0 frontend/build.sh
  11. +67 −0 frontend/package.json
  12. +41 −0 frontend/public/index.html
  13. +27 −0 frontend/public/js/content-scroll.js
  14. +261 −0 frontend/public/js/main.js
  15. +1 −0 frontend/public/js/main.min.js
  16. +25 −0 frontend/public/manifest.json
  17. +1 −0 frontend/public/media/elisa.json
  18. +1 −0 frontend/public/media/wave.json
  19. +3 −0 frontend/public/robots.txt
  20. +31 −0 frontend/readme.txt
  21. +2,354 −0 frontend/src/App.css
  22. +56 −0 frontend/src/App.tsx
  23. +27 −0 frontend/src/api.tsx
  24. BIN frontend/src/assets/Union.png
  25. +1 −0 frontend/src/assets/Union.svg
  26. BIN frontend/src/assets/Union.webp
  27. BIN frontend/src/assets/down.png
  28. +1 −0 frontend/src/assets/down.svg
  29. BIN frontend/src/assets/down.webp
  30. BIN frontend/src/assets/download.png
  31. +1 −0 frontend/src/assets/download.svg
  32. BIN frontend/src/assets/download.webp
  33. BIN frontend/src/assets/error_big.png
  34. BIN frontend/src/assets/error_big.webp
  35. BIN frontend/src/assets/error_small.png
  36. BIN frontend/src/assets/error_small.webp
  37. BIN frontend/src/assets/face_1.png
  38. BIN frontend/src/assets/face_1.webp
  39. BIN frontend/src/assets/face_2.png
  40. BIN frontend/src/assets/face_2.webp
  41. BIN frontend/src/assets/face_3.png
  42. BIN frontend/src/assets/face_3.webp
  43. BIN frontend/src/assets/fonts/Jost/Jost-Italic-VariableFont_wght.ttf
  44. BIN frontend/src/assets/fonts/Jost/Jost-VariableFont_wght.ttf
  45. +93 −0 frontend/src/assets/fonts/Jost/OFL.txt
  46. +81 −0 frontend/src/assets/fonts/Jost/README.txt
  47. BIN frontend/src/assets/fonts/Jost/static/Jost-Black.ttf
  48. BIN frontend/src/assets/fonts/Jost/static/Jost-BlackItalic.ttf
  49. BIN frontend/src/assets/fonts/Jost/static/Jost-Bold.ttf
  50. BIN frontend/src/assets/fonts/Jost/static/Jost-BoldItalic.ttf
  51. BIN frontend/src/assets/fonts/Jost/static/Jost-ExtraBold.ttf
  52. BIN frontend/src/assets/fonts/Jost/static/Jost-ExtraBoldItalic.ttf
  53. BIN frontend/src/assets/fonts/Jost/static/Jost-ExtraLight.ttf
  54. BIN frontend/src/assets/fonts/Jost/static/Jost-ExtraLightItalic.ttf
  55. BIN frontend/src/assets/fonts/Jost/static/Jost-Italic.ttf
  56. BIN frontend/src/assets/fonts/Jost/static/Jost-Light.ttf
  57. BIN frontend/src/assets/fonts/Jost/static/Jost-LightItalic.ttf
  58. BIN frontend/src/assets/fonts/Jost/static/Jost-Medium.ttf
  59. BIN frontend/src/assets/fonts/Jost/static/Jost-MediumItalic.ttf
  60. BIN frontend/src/assets/fonts/Jost/static/Jost-Regular.ttf
  61. BIN frontend/src/assets/fonts/Jost/static/Jost-SemiBold.ttf
  62. BIN frontend/src/assets/fonts/Jost/static/Jost-SemiBoldItalic.ttf
  63. BIN frontend/src/assets/fonts/Jost/static/Jost-Thin.ttf
  64. BIN frontend/src/assets/fonts/Jost/static/Jost-ThinItalic.ttf
  65. BIN frontend/src/assets/girl.png
  66. BIN frontend/src/assets/girl.webp
  67. BIN frontend/src/assets/heands.png
  68. BIN frontend/src/assets/heands.webp
  69. +1 −0 frontend/src/assets/loader.svg
  70. BIN frontend/src/assets/logo.png
  71. +16 −0 frontend/src/assets/logo.svg
  72. BIN frontend/src/assets/logo.webp
  73. +39 −0 frontend/src/assets/mos_logo.svg
  74. +1 −0 frontend/src/assets/mute.svg
  75. +1 −0 frontend/src/assets/newlogo.svg
  76. BIN frontend/src/assets/pause.png
  77. +1 −0 frontend/src/assets/pause.svg
  78. BIN frontend/src/assets/pause.webp
  79. +1 −0 frontend/src/assets/pause_big.svg
  80. +1 −0 frontend/src/assets/pause_small.svg
  81. BIN frontend/src/assets/pause_white.png
  82. +1 −0 frontend/src/assets/pause_white.svg
  83. BIN frontend/src/assets/pause_white.webp
  84. BIN frontend/src/assets/pin.gif
  85. BIN frontend/src/assets/play.png
  86. +1 −0 frontend/src/assets/play.svg
  87. BIN frontend/src/assets/play.webp
  88. BIN frontend/src/assets/track.png
  89. +1 −0 frontend/src/assets/track.svg
  90. BIN frontend/src/assets/track.webp
  91. BIN frontend/src/assets/track_big.png
  92. +1 −0 frontend/src/assets/track_big.svg
  93. BIN frontend/src/assets/track_big.webp
  94. +1 −0 frontend/src/assets/track_big_fill.svg
  95. +1 −0 frontend/src/assets/track_fill.svg
  96. BIN frontend/src/assets/union_white.png
  97. +1 −0 frontend/src/assets/union_white.svg
  98. BIN frontend/src/assets/union_white.webp
  99. BIN frontend/src/assets/up.png
  100. +1 −0 frontend/src/assets/up.svg
  101. BIN frontend/src/assets/up.webp
  102. BIN frontend/src/assets/upload.png
  103. +1 −0 frontend/src/assets/upload.svg
  104. BIN frontend/src/assets/upload.webp
  105. BIN frontend/src/assets/volume.png
  106. +1 −0 frontend/src/assets/volume.svg
  107. BIN frontend/src/assets/volume.webp
  108. BIN frontend/src/assets/wind_1.png
  109. BIN frontend/src/assets/wind_1.webp
  110. BIN frontend/src/assets/wind_2.png
  111. BIN frontend/src/assets/wind_2.webp
  112. +265 −0 frontend/src/components/player/PlayerNew.tsx
  113. +118 −0 frontend/src/components/player/Slider.tsx
  114. +4 −0 frontend/src/config.tsx
  115. +13 −0 frontend/src/index.css
  116. +17 −0 frontend/src/index.tsx
  117. +32 −0 frontend/src/layouts/Header.tsx
  118. +31 −0 frontend/src/layouts/Menu.tsx
  119. +19 −0 frontend/src/layouts/Title.tsx
  120. +156 −0 frontend/src/layouts/asr/Main.tsx
  121. +63 −0 frontend/src/layouts/asr/Message.tsx
  122. +305 −0 frontend/src/layouts/asr/Sidebar.tsx
  123. +67 −0 frontend/src/layouts/asr/ToggleText.tsx
  124. +59 −0 frontend/src/layouts/tts/Message.tsx
  125. +342 −0 frontend/src/layouts/tts/Sidebar.tsx
  126. +1 −0 frontend/src/logo.svg
  127. +54 −0 frontend/src/pages/Asr/Asr.tsx
  128. +108 −0 frontend/src/pages/Asr/actions.tsx
  129. +352 −0 frontend/src/pages/Asr/reducer.tsx
  130. +32 −0 frontend/src/pages/Asr/selectors.tsx
  131. +8 −0 frontend/src/pages/Documentaton.tsx
  132. +9 −0 frontend/src/pages/NoMatch.tsx
  133. +58 −0 frontend/src/pages/Tts/Tts.tsx
  134. +61 −0 frontend/src/pages/Tts/actions.tsx
  135. +81 −0 frontend/src/pages/Tts/reducer.tsx
  136. +16 −0 frontend/src/pages/Tts/selectors.tsx
  137. +1 −0 frontend/src/react-app-env.d.ts
  138. +15 −0 frontend/src/reportWebVitals.ts
  139. +23 −0 frontend/src/setupProxy.js
  140. +5 −0 frontend/src/setupTests.ts
  141. +20 −0 frontend/src/store.jsx
  142. +26 −0 frontend/tsconfig.json
  143. +6 −0 number_utils/russian_numbers.py
  144. +28 −28 number_utils/text2numbers.py
  145. +20 −0 punctuator_app.py
  146. +4 −1 requirements.txt
  147. +95 −21 speech_recognizer.py
  148. +107 −0 vad.py
181 changes: 87 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,108 @@
# SOVA ASR
# Система автопротоколирования конференций в онлайн режиме

SOVA ASR is a fast speech recognition solution based on [Wav2Letter](https://arxiv.org/abs/1609.03193) architecture. It is designed as a REST API service and it can be customized (both code and models) for your needs.
## Системные требования:
Операционная система, поддерживающая работу с Docker, предпочтительно Ubuntu 20.04, минимум 16 GB RAM, минимум 4 ядра, процессор с тактовой частотой не ниже 2.50 GHz, видеокарта NVIDIA с объёмом графической памяти не меньше 8 GB, 15 GB свободного места на SSD.

## Installation
Рекомендуемая конфигурация: инстанс типа g4dn.2xlarge в AWS с Ubuntu 20.04 и 50 GB SSD.

The easiest way to deploy the service is via docker-compose, so you have to install Docker and docker-compose first. Here's a brief instruction for Ubuntu:

#### Docker installation
## Инструкция по разворачиванию:
Клонируем репозиторий и переходим в папку проекта:
```
git clone https://github.com/sxdxfan/sova-asr
cd sova-asr
```

* Install Docker:
```bash
$ sudo apt-get update
$ sudo apt-get install \
Устанавливаем Docker и docker-compose с поддержкой NVIDIA:
```
sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
$ sudo usermod -aG docker $(whoami)
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $(whoami)
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \
sudo apt-key add -
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \
sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list
sudo apt-get update
sudo apt-get install nvidia-container-runtime
sudo echo -e '{\n "runtimes": {\n "nvidia": {\n "path": "nvidia-container-runtime",\n "runtimeArgs": []\n }\n },\n "default-runtime": "nvidia"\n}' >> /etc/docker/daemon.json
sudo systemctl restart docker.service
```
In order to run docker commands without sudo you might need to relogin.
* Install docker-compose:

Скачиваем и разворачиваем веса моделей:
```
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
wget http://dataset.sova.ai/SOVA-ASR/data.tar.gz
tar -xvf data.tar.gz && rm data.tar.gz
```

* (Optional) If you're planning on using CUDA run these commands:
Запускаем бэкенд часть (поднимутся сервисы на портах 8888, 8889, 8890):
```
$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \
sudo apt-key add -
$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
$ curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \
sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list
$ sudo apt-get update
$ sudo apt-get install nvidia-container-runtime
sudo docker-compose build
sudo docker-compose up -d sova-asr sova-asr-decoder sova-asr-punctuator
```
Add the following content to the file **/etc/docker/daemon.json**:
```json
{
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
},
"default-runtime": "nvidia"
}
```
Restart the service:
```bash
$ sudo systemctl restart docker.service
```

#### Build and deploy

**In order to run service with pretrained models you will have to download http://dataset.sova.ai/SOVA-ASR/data.tar.gz.**

* Clone the repository, download the pretrained models archive and extract the contents into the project folder:
```bash
$ git clone --recursive https://github.com/sovaai/sova-asr.git
$ cd sova-asr/
$ wget http://dataset.sova.ai/SOVA-ASR/data.tar.gz
$ tar -xvf data.tar.gz && rm data.tar.gz
Переходим в подпапку с фронтендом и устанавливаем зависимости:
```

* Build docker image
* If you're planning on using GPU (it is required for training and can be used for inference): build *sova-asr* image using the following command:
```bash
$ sudo docker-compose build
```
* If you're planning on using CPU only: modify `Dockerfile`, `docker-compose.yml` (remove the runtime and environment sections) and `config.ini` (*cpu* should be set to 0) and build *sova-asr* image:
```bash
$ sudo docker-compose build
```
* Run web service in a docker container
```bash
$ sudo docker-compose up -d sova-asr
```
## Testing
To test the service you can send a POST request:
```bash
$ curl --request POST 'http://localhost:8888/asr' --form 'audio_blob=@"data/test.wav"'
cd frontend
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install -y --upgrade npm node-gyp nodejs-dev libssl1.0-dev yarn
sudo npm install -g n
sudo n stable
yarn install
```

## Finetuning acoustic model
If you want to finetune the acoustic model you can set hyperparameters and paths to your own train and validation manifest files and run the training service.
* Set training options in *Train* section of **config.ini**. Train and validation csv manifest files should contain comma-separated audio file paths and reference texts in each line. For instance:
```bash
data/audio/000000.wav,добрый день
data/audio/000001.wav,как ваши дела
...
```
* Run training in docker container:
```bash
$ sudo docker-compose up -d sova-asr-train
```
Производим билд:
```
yarn build
```

## Customizations
После билда в папке фронтенда появится подпапка build, к которой необходимо указать путь в конфигурации веб сервера (например, nginx). Также необходимо сконфигурировать пути обращений к API бэкенда. Пример конфигурации nginx:

If you want to train your own acoustic model refer to [PuzzleLib tutorials](https://puzzlelib.org/tutorials/Wav2Letter/). Check [KenLM documentation](https://kheafield.com/code/kenlm/) for building your own language model. This repository was tested on Ubuntu 18.04 and has pre-built .so Trie decoder files for Python 3.6 running inside the Docker container, for modifications you can get your own .so files using [Wav2Letter++](https://github.com/facebookresearch/wav2letter) code for building Python bindings. Otherwise you can use a standard Greedy decoder (set in config.ini).
```
server {
index index.html index.php index.htm index.php;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
client_max_body_size 700M;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
location = /robots.txt {
add_header Content-Type text/plain;
return 200 "User-agent: *\nDisallow: /\n";
}
location / {
index index.html index.php index.htm index.php;
root /var/www/sova-asr/frontend/build;
client_max_body_size 256M;
try_files $uri $uri/ /index.html;
}
location /asr {
proxy_pass http://localhost:8888;
client_max_body_size 700M;
}
server_name SERVER_NAME;
listen 443 ssl http2;
ssl_certificate SSL_CERTIFICATE;
ssl_certificate_key SSL_CERTIFICATE_KEY;
access_log /var/log/nginx/asr-access.log;
error_log /var/log/nginx/asr-error.log;
}
```
10 changes: 7 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
@@ -13,19 +13,23 @@ def index():

@app.route('/asr', methods=['POST'])
def asr():
host_url = "https://asr-contest.nanosemantics.ai"
res = []
for f in request.files:
if f.startswith('audio_blob') and FileHandler.check_format(request.files[f]):

response_code, filename, response = FileHandler.get_recognized_text(request.files[f])
response_code, audio_file, docx_file, response = FileHandler.get_recognized_text(request.files[f])

if response_code == 0:
response_audio_url = url_for('media_file', filename=filename)
response_audio_url = url_for('media_file', filename=audio_file)
response_docx_url = url_for('media_file', filename=docx_file)
else:
response_audio_url = None
response_docx_url = None

res.append({
'response_audio_url': response_audio_url,
'response_docx_url': host_url + response_docx_url if response_docx_url else '',
'response_audio_url': host_url + response_audio_url if response_audio_url else '',
'response_code': response_code,
'response': response,
})
15 changes: 12 additions & 3 deletions config.ini
Original file line number Diff line number Diff line change
@@ -6,10 +6,10 @@ labels = [_-абвгдеёжзийклмнопрстуфхцчшщъыьэюя ]
model_path = data/w2l-16khz.hdf

# Path to language model
lm_path = data/vosk/lm.klm
lm_path = data/lm/lm.klm

# Path to the lexicon file
lexicon = data/vosk/lexicon.txt
lexicon = data/lm/lexicon.txt

# Path to prediction tokens file
tokens = data/tokens.txt
@@ -32,6 +32,15 @@ window_size = 0.02
# Window stride in seconds for acoustic model samples
window_stride = 0.01

# Voice Activity Detector agressiveness mode (0-3)
vad_aggressiveness_mode = 3

# Voice Activity Detector frame duration in milliseconds
vad_frame_duration_ms = 10

# Voice Activity Detector maximum pause duration in milliseconds
vad_max_pause_ms = 500


[Train]
# Path to train manifest csv
@@ -62,4 +71,4 @@ checkpoint_per_batch = 1000
save_folder = Checkpoints/

# Continue from checkpoint model
continue_from = data/w2l-16khz.hdf
continue_from = data/w2l-16khz.hdf
5 changes: 2 additions & 3 deletions data_loader.py
Original file line number Diff line number Diff line change
@@ -18,11 +18,10 @@ def load_audio(path, sample_rate):
sound = sound.set_channels(1)
sound = sound.set_sample_width(2)

return np.array(sound.get_array_of_samples()).astype(float)
return sound


def preprocess(audio_path, sample_rate=16000, window_size=0.02, window_stride=0.01, window='hamming'):
audio = load_audio(audio_path, sample_rate)
def preprocess(audio, sample_rate=16000, window_size=0.02, window_stride=0.01, window='hamming'):
nfft = int(sample_rate * window_size)
win_length = nfft
hop_length = int(sample_rate * window_stride)
21 changes: 13 additions & 8 deletions decoder.py
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ def decode(self, output, start_timestamp=0, frame_time=0.02):


class TrieDecoder:
def __init__(self, lexicon, tokens, lm_path, beam_threshold=30):
def __init__(self, lexicon, tokens, lm_path, beam_threshold=10):
from trie_decoder.common import Dictionary, create_word_dict, load_words
from trie_decoder.decoder import CriterionType, DecoderOptions, KenLM, LexiconDecoder
lexicon = load_words(lexicon)
@@ -101,12 +101,16 @@ def get_trie(self, lexicon):

return trie, sil_idx, blank_idx, unk_idx

def decode(self, output, start_timestamp=0, frame_time=0.02):
def decode(self, output, start_timestamp=0, frame_time=0.02, max_decoder_len=500):
output = np.log(softmax(output[:, :].astype(np.float32, copy=False), axis=-1))

t, n = output.shape
result = self.trieDecoder.decode(output.ctypes.data, t, n)[0]
tokens = result.tokens
results = []
for i in range(1 + output.shape[0] // max_decoder_len):
output_part = output[i * max_decoder_len:(i + 1) * max_decoder_len]
t, n = output_part.shape
results.append(self.trieDecoder.decode(output_part.ctypes.data, t, n)[0])

tokens = [token for result in results for token in result.tokens]

words, new_word = [], True
current_word, current_timestamp, start_idx, end_idx = None, start_timestamp, 0, 0
@@ -134,14 +138,15 @@ def decode(self, output, start_timestamp=0, frame_time=0.02):
words_len += end_idx - start_idx
words.append({
"word": current_word,
"start": np.round(current_timestamp, 2),
"timestamp": max(0.0, np.round(current_timestamp, 2) - 0.2),
"end": np.round(end_timestamp, 2),
"confidence": np.round(np.exp(word_lm_score / max(1, end_idx - start_idx)) * 100, 2)
"confidence": np.round(np.exp(word_lm_score / max(10, end_idx - start_idx)) * 100, 2)
})

else:
current_word += self.tokenDict.get_entry(k)

score = np.round(np.exp(result.score / max(1, words_len)), 2)
score = np.mean([result.score for result in results])
score = np.round(np.exp(score / max(1, words_len)), 2)

return DecodeResult(score, words)
31 changes: 31 additions & 0 deletions decoder_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import configparser
from decoder import TrieDecoder
from flask import Flask, request
import json
import numpy as np


config = configparser.ConfigParser()
config.read("config.ini", encoding="UTF-8")
lexicon = config["Wav2Letter"]["lexicon"]
tokens = config["Wav2Letter"]["tokens"]
lm_path = config["Wav2Letter"]["lm_path"]
beam_threshold = float(config["Wav2Letter"]["beam_threshold"])
decoder = TrieDecoder(lexicon, tokens, lm_path, beam_threshold)

app = Flask(__name__)


@app.route("/decode", methods=["POST"])
def decode():
data = request.json
outputs = np.array(data["outputs"])
result = decoder.decode(outputs, start_timestamp=data["start_timestamp"])

results = {
"text": result.text,
"score": result.score,
"words": result.words
}

return json.dumps(results, ensure_ascii=False)
Loading