在過去的幾個月里,我們中的許多人已經習慣于通過視頻電話看醫生。這當然很方便,但在通話結束后,醫生的重要建議就開始溜走了。我需要服用什么新藥?有什么副作用需要注意嗎?
Conversational AI 可以幫助構建一個應用程序來轉錄語音,并突出該轉錄本中的重要短語。 NVIDIA Riva 是一款 SDK ,它可以減少您構建和部署可用于這些任務的最先進的深度學習模型的時間。

在本文中,我們將向您展示如何構建一個 web 應用程序,該應用程序可以從實時視頻聊天中轉錄語音,并在轉錄本中標記關鍵短語。視頻聊天使用 PeerJS ,這是一個基于 WebRTC 的開源對等聊天框架。對于實時轉錄,您使用 Riva 中的自動語音識別( ASR )。標記成績單中的關鍵短語使用命名實體識別( NER ),也來自 Riva 。我們還向您展示了如何使用來自醫學領域的數據來訓練 NER 模型。雖然我們確實包含代碼示例,但為了清晰起見,我們省略了一些技術細節,因此我們鼓勵您看看 Riva Samples Docker 容器。
該應用程序的起點是一個簡單的點對點視頻通話 web 應用程序。它包含以下資源:
- 一個 HTML 頁面
- 一個客戶端 JavaScript 文件
- 一個服務器 JavaScript 文件,用于托管資產并設置對等連接
我們將教程保持在最低限度,因此請記住,真正的應用程序應該更加復雜。它將包括身份和會話管理、警報、分析和更強大的網絡處理。

在本文中,我們將重點介紹如何將 ASR 和 NLP 功能添加到 web 應用程序中,并跳過有關應用程序結構的一些細節。總結一下這個應用程序,它是一個簡單的服務器,在 Node . js 中實現,它使用 Express 托管 web 資產,使用 PeerJ 幫助客戶端在點對點 WebRTC 視頻聊天中相互連接。在客戶機上,瀏覽器加載網頁,然后與服務器對話以幫助建立與對等方的連接。建立對等連接后,兩個客戶端直接相互通信。視頻不再通過服務器路由。
此時,用戶可以加載網頁,聯系其他用戶,并進行實時視頻聊天。
添加 ASR 和 NLP
NVIDIA Riva 是一個 SDK ,可快速部署高性能對話式人工智能服務。 Riva quick start 參考資料提供了一個簡單易懂的指南,用于部署到 Riva 推理服務器。將資源下載到服務器后,可以歸結為幾個基本步驟:
- 在
config.sh
中配置部署。 - 通過運行
riva_init.sh
下載、優化和準備模型。 - 使用
riva_start.sh
啟動 Riva 技能服務器。
服務器啟動后,它會創建幾個 gRPC 端點,以幫助應用程序與 Riva 通信。為了確保一切正常工作,請嘗試從設置 Riva 的服務器啟動客戶端容器。致電 riva_start_client.sh
,然后查看示例客戶端,瀏覽筆記本,了解 Riva 提供的功能。

圖 3 顯示了應用程序的主要組件,現在您已經添加了 Riva 。聊天演示服務器( Node . js 應用程序)仍然設置視頻通話,現在它還與 Riva 服務器通信。
在該應用程序中,您可以使用 Riva 實現兩個功能:獲取對話的流媒體記錄,并在記錄中標記關鍵短語(命名實體)。為此,從客戶端提取音頻流,并將該音頻傳遞到 Node . js 服務器。服務器使用 gRPC 調用 Riva 來獲取成績單和命名實體,并將結果傳遞回客戶端。然后,客戶端可以在瀏覽器中呈現文本,并通過點對點連接傳遞文本,以便兩個用戶都可以看到整個對話。

從 web 客戶端獲取音頻
在客戶端,您可以通過點擊發送給對等方進行視頻聊天的本地 WebRTC 流來訪問音頻流。在客戶端 JavaScript 文件中,當用戶選擇時,初始化與服務器的 Riva 連接開始. 您正在通過套接字連接發送音頻數據,因此首先確保套接字處于活動狀態:
socket = socketio.on('connect', function() { console.log('Connected to speech server'); });
WebRTC audio 使用處理圖的概念。要在瀏覽器中使用音頻,請執行以下操作:
- 連接到音頻源,在本例中是從本地視頻聊天流。
- 創建一個處理節點來處理該音頻。
- 在您參與之前,將新節點連接回原始目的地,即音頻最初傳輸的地方。
每次您獲得進入新處理節點的完整音頻緩沖區時,請使用 web worker 重新采樣,并通過套接字連接將重新采樣的緩沖區發送到服務器。設置音頻源連接并初始化重采樣器:
audio_context = new AudioContext(); sampleRate = audio_context.sampleRate; let audioInput = audio_context.createMediaStreamSource(localStream); let bufferSize = 4096; let recorder = audio_context.createScriptProcessor(bufferSize, 1, 1); let worker = new Worker(resampleWorker); worker.postMessage({ command: 'init', config: { sampleRate: sampleRate, outputSampleRate: 16000 } });
每次緩沖區填滿時,瀏覽器都會觸發一個事件,因此告訴處理器節點如何處理它。使用輔助線程重新采樣,然后使用套接字連接將其傳遞給服務器:
recorder.onaudioprocess = function(audioProcessingEvent) { let inputBuffer = audioProcessingEvent.inputBuffer; worker.postMessage({ command: 'convert', // You only need the first channel buffer: inputBuffer.getChannelData(0) }); worker.onmessage = function(msg) { if (msg.data.command == 'newBuffer') { socket.emit('audio_in', msg.data.resampled.buffer); } }; };
在將音頻發送到 Riva 之前,不完全需要對其進行重新采樣。 Riva 可以自行進行重新采樣。但是,在瀏覽器中執行此操作既降低了帶寬要求,又簡化了從一個錄制源到另一個錄制源的一些差異。現在,您可以將新處理器節點連接到音頻圖中,包括源音頻和目標音頻:
audioInput.connect(recorder); recorder.connect(audio_context.destination);
此時,應用程序可以從用戶的麥克風中提取音頻,對流重新采樣,并使用套接字將重新采樣的音頻發送到服務器。接下來,我們將向您展示如何在服務器上使用此音頻。
將音頻路由到 Riva
在 Node . js 中實現的主服務器腳本使用 Express server 和 Socket . IO 來處理傳入的連接。當插座首次連接時,設置 Riva 連接。
io.on('connect', (socket) => { console.log('Client connected from %s', socket.handshake.address); // Initialize Riva socket.handshake.session.asr = new ASRPipe(); socket.handshake.session.asr.setupASR(); socket.handshake.session.asr.mainASR(function(result){ var nlpResult; if (result.transcript == undefined) { return; } // Final transcripts also get sent to NLP before returning if (result.is_final) { nlp.getRivaNer(result.transcript) .then(function(nerResult) { result.annotations = nerResult; socket.emit('transcript', result); }, function(error) { result.annotations = {err: error}; socket.emit('transcript', result); }); } else { socket.emit('transcript', result); } }); });
這里發生了一些事情。您可以創建一個新的 ASRPipe
并將其附加到套接字的 handshake.session
對象,這樣您就有一個單獨的 setupASR
流與每個客戶端連接關聯。使用 Riva 執行一些基本的 Riva 設置,然后啟動 ASR 偵聽循環。
ASR 偵聽循環是異步的。您定期向它發送成批的音頻數據,它通過回調函數定期發送結果。回調函數是傳遞給 mainASR
的函數。在流模式下, Riva 可以發回兩種結果:一種是臨時假設,隨著更多音頻的進入(提供更多的上下文),該假設會發生變化;另一種是最終的轉錄本。每當音頻中有短暫的停頓時,例如當演講者呼吸時,抄本往往會作為“最終”返回。您將這兩種結果都發送回客戶機,但當您獲得最終結果時,您也會將這些成績單發送到 NLP 服務以獲得 NER 。無論哪種方式,都可以使用 transcript
事件通過相同的套接字連接將結果傳遞回客戶端。
使用 Socket . IO ,可以為特定事件設置偵聽器。前面提到過其中一個事件: audio_in
事件,該事件在客戶端發送音頻數據包時觸發。在服務器端,將偵聽器添加到用于初始化 Riva 的相同 io.on('connect')
作用域。
socket.on('audio_in', (data) => { socket.handshake.session.asr.recognizeStream.write({audio_content: data}); });
這一部分很簡單,因為它不需要做很多事情。在連接套接字時設置了 Riva 流之后,您所要做的就是傳遞新的音頻內容。
發送語音識別請求
現在看看 gRPC 接口本身,從 ASR 開始。連接到 gRPC service using Node.js 時,主要有三個步驟:
- 使用協議緩沖區導入 Riva API 。
- 圍繞 API 編寫方便的函數。
- 在客戶端和 Riva 函數之間調解數據。
在 asr . js 模塊中,定義前面調用的 ASRPipe 類,首先導入 Riva API :
const jAudio = require('./protos/src/riva_proto/audio_pb'); var asrProto = 'src/riva_proto/riva_asr.proto'; var protoOptions = { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, includeDirs: [protoRoot] }; var asrPkgDef = protoLoader.loadSync(asrProto, protoOptions); var jAsr = grpc.loadPackageDefinition(asrPkgDef).nvidia.riva.asr;
然后,定義 ASRPipe
類以及前面調用的設置函數:
class ASRPipe { setupASR() { // the Riva ASR client this.asrClient = new jAsr.RivaSpeechRecognition(process.env.RIVA_API_URL, grpc.credentials.createInsecure()); this.firstRequest = { streaming_config: { config: { encoding: jAudio.AudioEncoding.LINEAR_PCM, sample_rate_hertz: 16000, language_code: ‘en-US’, max_alternatives: 1, enable_automatic_punctuation: true }, interim_results: true } }; } }
在這里,您創建一個 Riva ASR 客戶機并定義一些配置參數,這些參數在流打開時作為第一個請求發送到流。在同一類定義中,指定 mainASR
函數以設置實際 ASR 流:
async mainASR(transcription_cb) { this.recognizeStream = this.asrClient.streamingRecognize() .on('data', function(data){ if (data.results == undefined || data.results[0] == undefined) { return; } // transcription_cb is the result-handling callback transcription_cb({ transcript: data.results[0].alternatives[0].transcript, is_final: data.results[0].is_final }); }) .on('error', (error) => { console.log('Error via streamingRecognize callback'); console.log(error); }) .on('end', () => { console.log('StreamingRecognize end'); }); // First request to the stream is the configuration this.recognizeStream.write(this.firstRequest); }
streamingRecognize
函數是異步的。只要 Riva 有結果要發送,就會觸發數據事件,因此請重新打包這些結果,并將它們從早期發送到回調函數。
發送 NER 請求
調用 Riva NER 服務更簡單。像前面一樣加載 NLP API ,然后使用要處理的每行文本調用 ClassifyTokens
函數。每個請求發送文本以及要使用的 Riva – 部署模型。如果需要,在名為 computeSpans
的函數中進行一些后處理,然后傳遞結果。
function getRivaNer(text) { var entities; req = { text: , model: {model_name: process.env.RIVA_NER_MODEL} }; return new Promise(function(resolve, reject) { nlpClient.ClassifyTokens(req, function(err, resp_ner) { if (err) { reject(err); } else { entities = computeSpans(text, resp_ner.results[0].results); resolve({ner: entities}); } }); }); };
至此,您已經完成了對 Riva 的 gRPC 調用。您可以在客戶端捕獲音頻,通過流式連接將其發送到 Riva 以獲取轉錄本,并在文本中標記命名實體。每次 Riva 發回結果時,通過帶有 transcript
事件的套接字將結果傳遞給用戶的 web 客戶端。現在,通過在瀏覽器中處理這些結果來完成回路。
在瀏覽器中渲染結果
現在,您已經將帶有注釋的成績單返回到 web 客戶端,請在瀏覽器中顯示它們。回想一下,所有的客戶機 – 服務器通信都是通過 Socket . IO 連接進行的,因此請為帶有結果的 transcript
事件設置一個偵聽器。
socket.on('transcript', function(result) { document.getElementById('input_field').value = result.transcript; if (result.is_final) { // Erase input field $('#input_field').val(''); showAnnotatedTranscript(username, result.annotations, result.transcript); // Send the transcript to the peer to render if (peerConn != undefined && callActive) { peerConn.send({from: username, type: 'transcript', annotations: result.annotations, text: result.transcript}); } } });
input_field
元素在 web UI 中是一個方便的地方,可以顯示臨時記錄,在您講話時可以實時更新。在完整應用程序中,您使用同一字段發送純文本請求。當成績單標記為最終成績單時,將其顯示在單獨的框中,并將成績單副本發送給通話中的另一人,以便您可以看到對話的雙方。
呈現成績單本身是標準的 HTML 和 CSS 。為了讓您的生活更輕松,請使用優秀的 displaCy-ENT 將命名實體與文本對齊。
微調醫療設備的模型
默認情況下, Riva 提供了一個 NER 模型,該模型處理位置、人員、組織和時間等實體。這對于許多應用程序來說都很好,比如理解新聞文章和構建聊天機器人。早些時候,我們討論了對話 AI MIG ht 如何幫助遠程醫療應用程序。以下是 MIG ht 如何為 Riva 訓練一個 NER 模型來標記醫療實體。
從頭開始的培訓模型通常是一個時間密集型過程。您可以使用現有的經過訓練的模型并對自定義數據進行微調,而不是從新的模型開始。 NVIDIA TAO Toolkit 是一款基于 Python 的 AI 工具包,專門設計用于減少使用數據微調和定制預訓練模型所需的時間。
因為醫療數據可能高度敏感,所以在線查找并不總是那么容易。一個優秀的 NER 數據集來自 2010 i2b2/VA challenge ,其中包含針對問題(如疾病或癥狀)、治療(包括藥物)和測試標記的未識別醫生注釋。您可以申請訪問數據集,這是醫學 NLP 社區中使用的標準競爭基準。
NER 數據通常以某種形式的 IOB 提供 標記,其中文本中的每個標記標記為實體開頭、實體內部(不是第一個單詞)或外部。對于醫學文本,它通常如下所示:
正文:
DISCHARGE DIAGNOSES : Coronary artery disease , status post coronary artery bypass graft .
標簽:
O O O B-problem I-problem I-problem O O O B-treatment I-treatment I-treatment I-treatment O
這是用作 TAO 工具包輸入的數據。有關使用工具包培訓 NER 模型的更多信息,請參閱 TAO- Riva NER 集合中的培訓筆記本。在本例中,您從預訓練的語言模型檢查點 bert base uncased 開始,并使用預處理的 i2b2 數據為 NER 任務對其進行調優。
培訓和部署自定義模型需要幾個步驟。從預訓練的檢查點開始,您可以使用數據微調 TAO 工具箱中的模型。再次使用工具箱將模型導出為 Riva 的優化形式。為 Riva 提供一些基本部署設置,構建一個綁定配置的中間表單。然后,部署該包以創建一個正在運行的 Riva 服務器。有關更多信息,請參閱 NVIDIA Riva Speech Skills 。

以下命令使用 TAO 工具包對自定義數據上的預訓練模型進行微調:
!tlt token_classification train \ -e $SPECS_DIR/train.yaml \ # Specification file -g 1 \ -k $KEY \ -r $RESULTS_DIR/medical_ner \ data_dir={destination_mount}/data/i2b2 \ model.label_ids={destination_mount}/data/i2b2/label_ids.csv \ trainer.max_epochs=10
完成后, TAO 工具包將模型保存在名為 trained-model.tlt
的文件中。下一步是將此模型導出為 Riva 可用于部署的 riva
格式:
!tlt token_classification export \ -e $SPECS_DIR/export.yaml \ # Specification file -g 1 \ -m $RESULTS_DIR/medical_ner/checkpoints/trained-model.tlt \ -k $KEY \ -r $RESULTS_DIR/medical_ner \ export_format=RIVA
該模型現在導出為 exported-model.riva
,可在 Riva 中使用。
使用 Riva ServiceMaker Docker 映像,構建并部署新模型。
docker pull nvcr.io/riva/riva-speech:1.0.0b1-rc5-servicemaker docker run --gpus all -it --rm -v $RESULTS_DIR/medical_ner:/servicemaker-dev -v $RIVA_REPO_DIR:/data --entrypoint="/bin/bash" nvcr.io/ea-riva-stage/riva-service-maker:1.0.0b1-rc5 riva-build token_classification --IOB=true /data/med-ner.jmir /servicemaker-dev/exported-model.riva riva-deploy /data/med-ner.jmir /data/models -f
--IOB
標志告訴 Riva 將模型輸出解釋為 IOB 標記的 NER 模型,這簡化了模型輸出。 $RIVA_REPO_DIR
是 Riva 存儲庫的位置,該存儲庫是在從快速啟動腳本運行 riva_init.sh
時創建的。該存儲庫包含一個 models 子目錄,其中包含所有已部署的模型,包括默認的通用域。調用 riva-deploy
時, Riva 將新的 NER 模型插入該位置。
有了這個新的 NER 模型,你現在可以在應用程序中獲得醫學領域標簽,通過對話實時顯示。

部署到生產環境中
Riva 設計為高度可擴展,使用 Riva SDK 開發的應用程序可以部署在云端或本地 Kubernetes 集群中。 Riva 提供了一個示例頭盔圖,可用于入門:
在集群上安裝 Kubernetes 、 Helm 3 . 0 和 Kubernetes 的 NVIDIA GPU Operator 。接下來,從 NGC 下載 Riva AI 服務掌舵圖。
export NGC_API_KEY=<your_api_key> helm fetch https://helm.ngc.nvidia.com/ea-riva/charts/riva-api-0.1-ea.tgz --username='$oauthtoken' --password=<YOUR API KEY>
解開壓縮文件夾后,在 /riva-api
下查找部署所需的文件。
riva-api ├── Chart.yaml ├── templates │ ├── deployment.yaml │ ├── _helpers.tpl │ └── service.yaml └── values.yaml
Chart . yaml 文件包含有關頭盔部署的信息,如名稱、版本等。要更改部署配置,請查看 values . yaml 文件,并根據需要更改配置:
replicaCount
: Riva 服務副本的數量。speechServices [asr | nlp | tts]
:啟用語音服務的三個布爾參數。ngcModelConfigs
:要從 NGC 下載的型號配置。service
:要在生產中部署的負載平衡服務。
從 values . yaml 文件讀取值的 Kubernetes 部署文件位于 templates 文件夾中。 Kubernetes 集群上的 Riva 示例部署執行以下操作:
- 找到 GPU 節點并使用預訓練模型拉取 Riva 語音 Docker 容器。
- 裝載包含模型目錄的 Docker 卷。
- 拉動、設置和運行 Triton 推理服務器。
- 為入站推斷請求和出站響應打開端口。
- 設置 Prometheus 服務以提取 GPU 和推斷度量。
最后,要部署 Riva 服務器,請運行以下命令:
helm install riva_server riva-api
或者,使用 --set
選項安裝,而不修改 values . yaml 文件。確保正確設置 NGC _ API _鍵 ngcCredentials.email
和 model_key_string
值。默認情況下, model_key_string
選項設置為 tlt_encode.
helm install riva-api --set ngcCredentials.password=`echo -n $NGC_API_KEY | base64 -w0` --set ngcCredentials.email=your_email@your_domain.com --set modelRepoGenerator.modelDeployKey=`echo -n model_key_string | base64 -w0` > NAME: riva-api LAST DEPLOYED: Thu Jan 28 12:05:36 2021 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None
檢查日志,查看 Riva 服務器是否已部署且沒有任何錯誤:
kubectl get pods kubectl logs <pod name>
要向 Riva 服務器發出推斷請求,必須獲取負載平衡器的 IP 地址:
kubectl get services > NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE riva-api LoadBalancer 10.100.194.170 ac51c23e62d094aa68ac2adb98edb7eb-798330929.us-east-2.elb.amazonaws.com 8000:30034/TCP,8001:31749/TCP,8002:30708/TCP,50051:30513/TCP,60051:31739/TCP 2m19s kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 123m
EXTERNAL-IP
值可以在 env.txt
中用作 external endpoint:
RIVA_API_URL= <external-IP>
在理想的微服務部署架構中,示例 web 應用程序也應包含在 Helm 部署中。但是,對于本文,請將 Node . js 應用程序排除在集群環境之外。使用示例應用程序中的上一個命令中的群集 IP 地址,并測試 Riva ASR 和 NLP 的規模能力。
結論
很難構建一個針對您的用例定制的高性能、可伸縮的對話 AI 應用程序。在本文中,我們討論了如何使用 NVIDIA Riva 輕松地向現有應用程序添加音頻轉錄和命名實體識別功能。我們還介紹了如何使用 TAO Toolkit 定制應用程序,以及如何使用 Helm 圖表大規模部署應用程序。您可以從 下載 Riva 開始學習。