写点什么

如何使用开源 Rasa 和 Mozilla 工具构建语音助手

  • 2019-10-28
  • 本文字数:9752 字

    阅读完需:约 32 分钟

如何使用开源Rasa和Mozilla工具构建语音助手

像谷歌助手这样的平台简化了自定义语音助手的构建。但是,如果你想要构建一个在本地运行并能确保数据隐私的助手,该怎么办呢?你可以使用 Rasa、Mozilla DeepSpeech 和 Mozilla TTS 等开源工具来实现。通过本教程,你可以了解实现过程。


随着谷歌助手和 Alexa 等平台越来越受欢迎,语音助手必将成为各行各业客户互动的下一个重要领域。然而,除非你使用托管的现成解决方案,否则语音助手的开发将带来一系列超越 NLU 和对话管理的全新挑战——除此之外,你还需要照顾到语音转文本、文本转语音组件以及前端。不久前,当我们尝试构建一个基于Rasa的谷歌助手时,我们谈到了语音主题。像谷歌助手这样的平台消除了实现语音处理和前端组件的障碍,但是它迫使你在数据的安全性和所用工具的灵活性上做出妥协。


那么,如果你想要构建一个本地运行并能确保数据安全的语音助手,你有哪些选项呢?我们来看看。在本文中,你将了解仅使用开源工具如何构建语音助手——从后端到前端。


本文要点

  1. 工具和软件概述

  2. Rasa 助手

  3. 实现语音转文本组件

  4. 实现文本转语音组件

  5. 组合到一起

  6. 下一步工作

  7. 小结及相关资源

1. 工具和软件概述

本文的目的是向你展示仅使用开源工具如何构建自己的语音助手。一般来说,构建语音助手需要五个主要组件:


  1. 语音界面——供用户和助手通信的前端(Web、移动应用、智能扬声器等);

  2. 语音转文本(STT)——一个语音处理组件,将用户的音频输入转换为文本表示;

  3. NLU——该组件从用户输入文本中提取结构化数据(意图和实体),帮助助手理解用户意图;

  4. 对话管理——该组件决定助手在特定的对话状态中如何响应并生成响应文本;

  5. 文本转语音(TTS)——该组件获取助手的文本响应,生成语音表示然后返回给用户。



虽然对于 NLU 和对话管理来说,开源的Rasa是一个显而易见的选择,但是 STT 和 TTS 的选择就比较困难了,因为没有那么多开源框架可供选择。在研究了当前可用的选项 CMUSphinx、Mozilla DeepSpeech、Mozilla TTS、Kaldi 之后,我们决定使用 Mozilla 的工具——Mozilla DeepSpeechMozilla TTS,原因如下:


  • Mozilla 的工具提供了一组预先训练好的模型,但是你也可以使用定制的数据训练自己的模型。这就让你可以快速实现一些东西,但也让你可以自由地构建自定义组件。

  • 与其他可选工具相比,Mozilla 的工具似乎是操作系统无关度最高的。

  • 两个工具都是用 Python 编写的,与 Rasa 集成稍微容易些。

  • 有一个大而活跃的开源社区时刻准备着帮助回答技术问题。


Mozilla DeepSpeech 和 Mozilla TTS 是什么?Mozilla DeepSpeech 是一个语音转文本的框架,它接收用户的音频输入,并使用机器学习将其转换为文本格式,稍后由 NLU 和对话系统进行处理。Mozilla TTS 则负责相反的工作——它接收文本输入(在我们的例子中是对话系统生成的助手响应),并使用机器学习创建文本的音频表示。


NLU、对话管理和语音处理组件处理语音助手的后端,那么前端呢?好吧,这就是最大的问题所在——如果你搜索开源语音界面小部件,最终很可能没有结果。至少我们遇到了这样的情况,这也是为什么我们开发了自己的语音界面 Rasa,我们将在这个项目中使用它,并且很高兴能与社区分享!


综上所述,开源语音助手包含以下组成部分:



Rasa 助手

对于这个项目,我们将使用一个已有的 Rasa 助手——Sara。它是一个基于 Rasa 的开源助手,可以回答关于 Rasa 框架的各种问题来帮助你入门。下面是和 Sara 对话的一个例子:



下面是在本地机器上安装 Sara 的步骤:


  1. 克隆 Sara 库:


git clone https://github.com/RasaHQ/rasa-demo.gitcd rasa-demo
复制代码


  1. 安装必要的依赖:


pip install -e .
复制代码


  1. 训练 NLU 和对话模型:


rasa train --augmentation 0
复制代码


  1. 在你的终端上测试 Sara:


docker run -p 8000:8000 rasa/ducklingrasa run actions --actions demo.actionsmake run-cmdline
复制代码


为了把 Sara 变成一个语音助手,我们必须在实现的后期编辑一些项目文件。在此之前,让我们先实现 TTS 和 STT 组件。

实现语音转文本组件

我们接下来要实现语音转文本组件——Mozilla DeepSpeech模型。阅读 Rouben Morais 的这篇博文,进一步了解 Mozilla DeepSpeech 的工作原理。Mozilla DeepSpeech 提供了一些预训练的模型,也允许你训练自己的模型。简单起见,我们在这个项目中使用了一个预先训练好的模型。以下是在本地机器上安装 STT 的步骤:


  1. 安装 Mozilla DeepSpeech:


pip3 install deepspeech
复制代码


  1. 下载预训练的语音转文本模型并解压到项目目录下:


wget https://github.com/mozilla/DeepSpeech/releases/download/v0.5.1/deepspeech-0.5.1-models.tar.gztar xvfz deepspeech-0.5.1-models.tar.gz
复制代码


运行上述命令后,项目目录下会新增一个目录 deepspeech-0.5.1-models,其中包含模型文件。


  1. 测试模型:


检查组件设置是否正确的最佳方法是使用一些音频输入示例测试模型。下面是测试脚本:


  • 函数 record_audio()获取一段时长 5 秒的音频并保存到文件 test_audio.wav 中;

  • 函数 deepspeech_predict()加载 DeepSpeech 模型并将文件 test_audio.wav 传递给它,它会预测音频输入的文本。


import pyaudiofrom deepspeech import Modelimport scipy.io.wavfile as wav

WAVE_OUTPUT_FILENAME = "test_audio.wav"
def record_audio(WAVE_OUTPUT_FILENAME): CHUNK = 1024 FORMAT = pyaudio.paInt16 CHANNELS = 1 RATE = 16000 RECORD_SECONDS = 5
p = pyaudio.PyAudio()
stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)
print("* recording")
frames = [stream.read(CHUNK) for i in range(0, int(RATE / CHUNK * RECORD_SECONDS))]
print("* done recording")
stream.stop_stream() stream.close() p.terminate()
wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb') wf.setnchannels(CHANNELS) wf.setsampwidth(p.get_sample_size(FORMAT)) wf.setframerate(RATE) wf.writeframes(b''.join(frames)) wf.close()
def deepspeech_predict(WAVE_OUTPUT_FILENAME):
N_FEATURES = 25 N_CONTEXT = 9 BEAM_WIDTH = 500 LM_ALPHA = 0.75 LM_BETA = 1.85

ds = Model('deepspeech-0.5.1-models/output_graph.pbmm', N_FEATURES, N_CONTEXT, 'deepspeech-0.5.1-models/alphabet.txt', BEAM_WIDTH)
fs, audio = wav.read(WAVE_OUTPUT_FILENAME) return ds.stt(audio, fs)
if __name__ == '__main__': record_audio(WAVE_OUTPUT_FILENAME) predicted_text = deepspeech_predict(WAVE_OUTPUT_FILENAME) print(predicted_text)
复制代码


使用下面的命令运行脚本,一旦你看到消息“录制……”就说一个你希望用于测试这个模型的句子:


python deepspeech_test_prediction.py
复制代码


在本文的下一部分中,你将学习如何设置项目的第三部分——文本转语音组件。

实现文本转语音组件

为了使助手能够用语音而不是文本进行响应,我们必须设置文本转语音组件,该组件将接收 Rasa 生成的文本响应并将其转换为声音。为此,我们使用Mozilla TTS。就像 Mozilla DeepSpeech 一样,它提供了预训练模型,但是你也可以使用自定义数据训练自己的模型。这次,我们还是使用一个预先训练好的 TTS 模型。下面是在本地机器上设置 TTS 组件的步骤:


  1. 克隆 Mozilla TTS 库:


git clone https://github.com/mozilla/TTS.gitcd TTSgit checkout db7f3d3
复制代码


  1. 安装程序包:


python setup.py develop 
复制代码


  1. 下载模型:


在 Sara 目录下,创建一个文件夹 tts_mode*l ,并从这里下载模型文件:


https://drive.google.com/drive/folders/1GU8WGix98WrR3ayjoiirmmbLUZzwg4n0


  1. 测试组件:


使用以下脚本测试文本转语音组件。下面是脚本完成的工作:


  • 函数 load_model()加载模型并做好处理准备;

  • 函数 tts()获取文本输入并创建一个音频文件 test_tts.wav。


如果你希望使用自定义的输入来测试模型,则可以修改 sentence 变量。脚本运行结束后,结果将保存在 test_tt .wav 文件中,你可以听下该文件来查看模型的效果。


import osimport sysimport ioimport torchfrom collections import OrderedDict
from TTS.models.tacotron import Tacotronfrom TTS.layers import *from TTS.utils.data import *from TTS.utils.audio import AudioProcessorfrom TTS.utils.generic_utils import load_configfrom TTS.utils.text import text_to_sequencefrom TTS.utils.synthesis import synthesisfrom utils.text.symbols import symbols, phonemesfrom TTS.utils.visual import visualize
# 设置常量MODEL_PATH = './tts_model/best_model.pth.tar'CONFIG_PATH = './tts_model/config.json'OUT_FILE = 'tts_out.wav'CONFIG = load_config(CONFIG_PATH)use_cuda = False
def tts(model, text, CONFIG, use_cuda, ap, OUT_FILE): waveform, alignment, spectrogram, mel_spectrogram, stop_tokens = synthesis(model, text, CONFIG, use_cuda, ap) ap.save_wav(waveform, OUT_FILE) return alignment, spectrogram, stop_tokens

def load_model(MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE): # 加载模型 num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols) model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)
# 加载音频处理器 # CONFIG.audio["power"] = 1.3 CONFIG.audio["preemphasis"] = 0.97 ap = AudioProcessor(**CONFIG.audio)
# 加载模型状态 if use_cuda: cp = torch.load(MODEL_PATH) else: cp = torch.load(MODEL_PATH, map_location=lambda storage, loc: storage)
# 加载模型 model.load_state_dict(cp['model']) if use_cuda: model.cuda() model.eval()
model.eval() model.decoder.max_decoder_steps = 1000 align, spec, stop_tokens = tts(model, sentence, CONFIG, use_cuda, ap, OUT_FILE)
if __name__ == '__main__': sentence = "Hello, how are you doing? My name is Sara" load_model(MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE)
复制代码


到这里,你本地的机器上应该已经运行着所有最重要的组件——Rasa 助手、语音转文本和文本转语音组件。接下来要做的就是将所有这些组件组合到一起,并将助手连接到 Rasa 语音界面。要了解如何实现,请阅读本文的下一部分。

组合到一起

要把这些组件组合到一起对语音助手进行实际的测试,我们还需要两个东西:


(1)语音界面;


(2)一个可以在 UI 和后端(Mozilla 和 Rasa 组件)之间建立通信的连接器。


我们先设置下 Rasa 语音界面,步骤如下:


  1. 按照这里的说明安装 npm 和 node;

  2. 克隆 Rasa Voice UI 库:


git clone https://github.com/RasaHQ/rasa-voice-interface.gitcd rasa-voice-interface
复制代码


  1. 安装组件:


npm install
复制代码


  1. 测试组件:


npm run serve
复制代码


运行上述命令后,打开浏览器并导航到https://localhost:8080,检查语音界面是否正在加载。弹跳球表示它已成功加载并在等待连接。



为了将助手连接到界面,你需要一个连接器。该连接器还将确定用户说话时引发什么动作,以及音频响应如何传回前端组件。要创建一个连接器,我们可以使用现有的socketio连接器,并使用几个新组件升级它:


  • SocketIOInput()类事件 ‘user_utter’ 已经更新为接收 Rasa 语音界面以链接形式发送的音频数据并保存为磁盘上的一个.wav 文件。然后,我们加载 Mozilla STT 模型将音频转换为文本表示并传递给 Rasa;

  • SocketIOutput()类获得一个新方法_send_audio_message(),该方法取得 Rasa 对话管理模型预测生成的文本响应,加载 Mozilla TTS 模型将文本转换为音频格式并发回前端。


下面是升级后连接器的全部代码:


import loggingimport uuidfrom sanic import Blueprint, responsefrom sanic.request import Requestfrom socketio import AsyncServerfrom typing import Optional, Text, Any, List, Dict, Iterable
from rasa.core.channels.channel import InputChannelfrom rasa.core.channels.channel import UserMessage, OutputChannel
import deepspeechfrom deepspeech import Modelimport scipy.io.wavfile as wav
import osimport sysimport ioimport torchimport timeimport numpy as npfrom collections import OrderedDictimport urllib
import librosa
from TTS.models.tacotron import Tacotronfrom TTS.layers import *from TTS.utils.data import *from TTS.utils.audio import AudioProcessorfrom TTS.utils.generic_utils import load_configfrom TTS.utils.text import text_to_sequencefrom TTS.utils.synthesis import synthesisfrom utils.text.symbols import symbols, phonemesfrom TTS.utils.visual import visualize

logger = logging.getLogger(__name__)
def load_deepspeech_model(): N_FEATURES = 25 N_CONTEXT = 9 BEAM_WIDTH = 500 LM_ALPHA = 0.75 LM_BETA = 1.85
ds = Model('deepspeech-0.5.1-models/output_graph.pbmm', N_FEATURES, N_CONTEXT, 'deepspeech-0.5.1-models/alphabet.txt', BEAM_WIDTH) return ds

def load_tts_model():
MODEL_PATH = './tts_model/best_model.pth.tar' CONFIG_PATH = './tts_model/config.json' CONFIG = load_config(CONFIG_PATH) use_cuda = False
num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols) model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)

num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols) model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)
# 加载音频处理器 # CONFIG.audio["power"] = 1.3 CONFIG.audio["preemphasis"] = 0.97 ap = AudioProcessor(**CONFIG.audio)

# 加载模型状态 if use_cuda: cp = torch.load(MODEL_PATH) else: cp = torch.load(MODEL_PATH, map_location=lambda storage, loc: storage)
# 加载模型 model.load_state_dict(cp['model']) if use_cuda: model.cuda()

#model.eval() model.decoder.max_decoder_steps = 1000 return model, ap, MODEL_PATH, CONFIG, use_cuda
ds = load_deepspeech_model()model, ap, MODEL_PATH, CONFIG, use_cuda = load_tts_model()

class SocketBlueprint(Blueprint): def __init__(self, sio: AsyncServer, socketio_path, *args, **kwargs): self.sio = sio self.socketio_path = socketio_path super(SocketBlueprint, self).__init__(*args, **kwargs)
def register(self, app, options): self.sio.attach(app, self.socketio_path) super(SocketBlueprint, self).register(app, options)

class SocketIOOutput(OutputChannel):
@classmethod def name(cls): return "socketio"
def __init__(self, sio, sid, bot_message_evt, message): self.sio = sio self.sid = sid self.bot_message_evt = bot_message_evt self.message = message

def tts(self, model, text, CONFIG, use_cuda, ap, OUT_FILE): import numpy as np waveform, alignment, spectrogram, mel_spectrogram, stop_tokens = synthesis(model, text, CONFIG, use_cuda, ap) ap.save_wav(waveform, OUT_FILE) wav_norm = waveform * (32767 / max(0.01, np.max(np.abs(waveform)))) return alignment, spectrogram, stop_tokens, wav_norm

def tts_predict(self, MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE):
align, spec, stop_tokens, wav_norm = self.tts(model, sentence, CONFIG, use_cuda, ap, OUT_FILE) return wav_norm

async def _send_audio_message(self, socket_id, response, **kwargs: Any): # type: (Text, Any) -> None """Sends a message to the recipient using the bot event."""
ts = time.time() OUT_FILE = str(ts)+'.wav' link = "http://localhost:8888/"+OUT_FILE
wav_norm = self.tts_predict(MODEL_PATH, response['text'], CONFIG, use_cuda, OUT_FILE)

await self.sio.emit(self.bot_message_evt, {'text':response['text'], "link":link}, room=socket_id)


async def send_text_message(self, recipient_id: Text, message: Text, **kwargs: Any) -> None: """Send a message through this channel."""
await self._send_audio_message(self.sid, {"text": message})




class SocketIOInput(InputChannel): """A socket.io input channel."""
@classmethod def name(cls): return "socketio"
@classmethod def from_credentials(cls, credentials): credentials = credentials or {} return cls(credentials.get("user_message_evt", "user_uttered"), credentials.get("bot_message_evt", "bot_uttered"), credentials.get("namespace"), credentials.get("session_persistence", False), credentials.get("socketio_path", "/socket.io"), )
def __init__(self, user_message_evt: Text = "user_uttered", bot_message_evt: Text = "bot_uttered", namespace: Optional[Text] = None, session_persistence: bool = False, socketio_path: Optional[Text] = '/socket.io' ): self.bot_message_evt = bot_message_evt self.session_persistence = session_persistence self.user_message_evt = user_message_evt self.namespace = namespace self.socketio_path = socketio_path

def blueprint(self, on_new_message): sio = AsyncServer(async_mode="sanic") socketio_webhook = SocketBlueprint( sio, self.socketio_path, "socketio_webhook", __name__ )
@socketio_webhook.route("/", methods=['GET']) async def health(request): return response.json({"status": "ok"})
@sio.on('connect', namespace=self.namespace) async def connect(sid, environ): logger.debug("User {} connected to socketIO endpoint.".format(sid)) print('Connected!')
@sio.on('disconnect', namespace=self.namespace) async def disconnect(sid): logger.debug("User {} disconnected from socketIO endpoint." "".format(sid))
@sio.on('session_request', namespace=self.namespace) async def session_request(sid, data): print('This is sessioin request')
if data is None: data = {} if 'session_id' not in data or data['session_id'] is None: data['session_id'] = uuid.uuid4().hex await sio.emit("session_confirm", data['session_id'], room=sid) logger.debug("User {} connected to socketIO endpoint." "".format(sid))
@sio.on('user_uttered', namespace=self.namespace) async def handle_message(sid, data):
output_channel = SocketIOOutput(sio, sid, self.bot_message_evt, data['message']) if data['message'] == "/get_started": message = data['message'] else: ##receive audio received_file = 'output_'+sid+'.wav'
urllib.request.urlretrieve(data['message'], received_file) path = os.path.dirname(__file__)
fs, audio = wav.read("output_{0}.wav".format(sid)) message = ds.stt(audio, fs)
await sio.emit(self.user_message_evt, {"text":message}, room=sid)

message_rasa = UserMessage(message, output_channel, sid, input_channel=self.name()) await on_new_message(message_rasa)
return socketio_webhook
复制代码


将上述代码保存为项目目录下的一个文件 socketio_connector.py。


在开始试用之前,还有最后一件事需要完成,就是连接器配置——既然我们已经构建了一个自定义的连接器,所以我们必须告诉 Rasa 使用这个自定义连接器接收用户输入并发回响应。为此,在 Sara 的项目目录中创建一个 credentials.yml 文件,并提供以下详细信息(这里的 socketio_connector 是实现自定义连接器的模块的名称,而 SocketIOInput 是自定义连接器的输入类的名称):


  socketio_connector.SocketIOInput:    bot_message_evt: bot_uttered    session_persistence: true    user_message_evt: user_uttered
复制代码


至此,所有工作就已经完成了!剩下的就是启动助手并与它进行对话,步骤如下:


  1. 进入工作目录,在服务器上启动 Rasa 助手:


rasa run --enable-api -p 5005
复制代码


  1. 启动 Rasa 的自定义 action 服务器:


rasa run actions --actions demo.actions
复制代码


  1. DucklingHTTPExtractor 是 Sara 的其中一个组件。要使用该组件,运行以下命令启动该组件的服务器:


docker run -p 8000:8000 rasa/duckling
复制代码


  1. 启动一个简单的 http 服务器,用于向客户端发送音频文件:


python3 -m http.server 8888
复制代码


如果你在浏览器中刷新 Rasa 语音界面,就会看到我们构建的语音助手已经准备好对话:



点击开始,与仅使用开源工具构建的语音助手进行对话!

下一步工作

语音助手开发带来了一系列全新的挑战——不仅仅是有效的 NLU 和对话,你还需要有效的 STT 和 TTS 组件,而且,你的 NLU 必须足够灵活,可以弥补 STT 所犯的错误。如果你跟随本教程做了这个项目,就会注意到这个助手并不完美,它还有很大的改进空间,尤其是在 STT 和 NLU 阶段。那么如何改进呢?以下是一些建议:


  • 预先训练好的 STT 模型是针对非常通用的数据进行训练的,这使得该模型在更具体的领域中比较容易出错。使用 Mozilla DeepSpeech 构建自定义 STT 模型可以提高 STT 和 NLU 的性能。

  • 改进 NLU 大概可以弥补 STT 所犯的一些错误。提高 NLU 模型性能有一个相当简单的方法,就是增加训练数据,为每个意图提供更多的示例,并向 Rasa NLU 管道添加拼写检查器来纠正一些比较小的 STT 错误。

小结及相关资源

在 Rasa,为了使开发人员能够构建出了不起的东西,我们一直在寻找方法突破工具和软件的极限。我们希望通过构建这个项目向你展示,使用 Rasa 不仅可以构建文本助手,还可以构建语音助手。同时,我们也希望可以为你带来灵感,让你构建出了不起的应用程序,而又不损害你所使用的工具的安全性和灵活性。你用 Rasa 构建语音助手了吗?你用了什么工具?请在 Rasa 社区论坛上分享你的经验。



查看英文原文:


https://blog.rasa.com/how-to-build-a-voice-assistant-with-open-source-rasa-and-mozilla-tools/


2019-10-28 14:072520

评论

发布
暂无评论
发现更多内容

冰河公开了其总结的一项重要的编程技能!

冰河

Java 正则表达式 程序员

利用区块链技术,打造绿色发展的中药材生态链

CECBC

中药材

电子门锁没电的解决办法

孙叫兽

生活 程序人生 电子锁

【得物技术】TDengine在得物的落地应用

得物技术

数据库 tdengine 数据 sentinel 得物技术

安全高可用通信背后的 MySQL 优化实践

融云 RongCloud

看了上百份面经,发现每次面试的问题都逃不过这几方面

yes

面试

FFmpeg入门篇

Changing Lin

ffmpeg media

xshell不能输入命令字符了?

Geek_6370d5

xshell 错误集锦

朱嘉明:全面认知区块链的科学特征

CECBC

区块链

微信小程序开发:如何快速实现添加一条分割线的项目需求

三掌柜

微信小程序 3月日更

高质量、高并发的实时通信架构设计与探索

融云 RongCloud

实时数据流计算引擎Flink和Spark流计算对比

小舰

大数据 flink spark 流计算

深入剖析数据库事务的隔离级别

小舰

数据库 事务隔离级别 数据库事务

GO训练营第11周——DNS&CDN&多活架构

Glowry

打卡学习 VBA 和 PYTHON week01-02

小怪兽

3月日更 【IT蜗壳教学】 【自我错误点总结】

产品调研报告

Ashley.

白话讲解,拜占庭将军问题

架构精进之路

分布式 算法 3月日更

初识Golang之安装运行篇

Kylin

3月日更 21天挑战 Go 语言

2021 年要了解的 34 种 JavaScript 简写优化技术

LeanCloud

JavaScript 面试 大前端

全票通过!微众开源项目EventMesh进入Apache孵化器

康月牙

Apache 开源 Event 事件网格 eventmesh

堪称完美!中国Java之父纯手打,阿里首发:多线程与高并发

Java架构之路

Java 程序员 架构 面试 编程语言

GO训练营第12、13周—— runtime

Glowry

拍乐云推出“实时标注”,助力少儿编程、在线金融等行业场景实现高效互动

拍乐云Pano

音视频 在线教育 互动白板 实时标注 在线金融

我看 JAVA 之 引用类型(Reference)

awen

Java ThreadLocal Reference

2021金三银四春招来袭,掌握这份成长宝典,offer岂不是随便拿?

Java架构之路

Java 程序员 架构 面试 编程语言

如何在 Spring 生态中玩转 RocketMQ?

阿里巴巴云原生

Java Serverless 微服务 云原生 中间件

寻找被遗忘的勇气(十九)

Changing Lin

3月日更

炸了,炸了!阿里P9纯手打Java面试小抄(21版)在GitHub上已获80万star

Java架构之路

Java 程序员 架构 面试 编程语言

标准引领 浪潮工业互联网助力澳门质量品牌国际认证联盟成立

浪潮云

工业互联网

如何使用开源Rasa和Mozilla工具构建语音助手_AI_Justina Petraityte_InfoQ精选文章