这是本系列的第二篇,第一篇我们完成了将MP4视频转换为PCM音频,这篇我们实现基于百度云的录音转写,本文所有源代码参见:https://gitee.com/coolpine/thomas
第一篇中,我们转换后的PCM文件,还是存储在本地文件系统中。接下来,我们需要基于百度云的对象存储BOS服务,将文件上传到云端:
具体服务开通过程忽略,补充说明下,选择百度云是因为语音转录是免费的,BOS虽然收费,但非常便宜,从本项目情况看,总共320MB左右的文件,一共花费不到1元钱,简直白菜价了。
先是获取到相关key后,在properties中配置进去:
#百度云BOS
thomas.bos.access-key-id=xxx
thomas.bos.secret-access-key=xx
thomas.bucket-name=xxx
具体引入的依赖是:
<groupId>com.baidubcegroupId>
<artifactId>bce-java-sdkartifactId>
<version>0.10.105version>
特别提示下,该依赖会连带引入很多第三方依赖,在通过maven-helper插件分析依赖时,发现很多依赖冲突的,例如log4j、commons-logging、slf4j-log4j12等,建议一并排除掉。
同时,因为本工程并未直接依赖com.google.guava,但在bce-java-sdk中,也存在该依赖冲突。参考的解决办法是:先在bce-java-sdk中排除com.google.guava依赖,同时单独再引入com.google.guava:
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>17.0version>
dependency>
本项目中,我已将相关功能封装到了BosFileService中,主要是基于BosClient进行文件操作:
1、获取bucket下所有文件:
bosClient.listObjects(THOMAS_BUCKET_NAME).getContents();
2、基于文件key获取7天有效期的URL:
bosClient.generatePresignedUrl(THOMAS_BUCKET_NAME, objectKey, 7 * 24 * 60 * 60);
3、上传单个文件:
PutObjectResponse response = bosClient.putObject(THOMAS_BUCKET_NAME, key, filePath.toFile());
4、上传成功时,会返回eTag,将之记录到本地数据库:
fileUploadRepo.save(SpeechFileUploadInfo.builder().eTag(eTag).fileName(fileName).build());
5、批量上传目录下所有文件:
Files.list(rootPath).forEach(path -> {
if (Files.isDirectory(path)) {
//递归遍历目录
count.addAndGet(batchUploadFile(path));
} else {
//上传该文件
count.getAndAdd(uploadFile(path));
}
});
完成文件上传到云端BOS后,接下来基于百度云AI的语音识别(录音转写)服务,提交离线转写任务:
首先,将ai应用相关key记录在properties文件中,同时也一并记录相关api的调用路径:
thomas.ai.api-key=xxx
thomas.ai.secret-key=xxx
thomas.ai.access-url=https://aip.baidubce.com/oauth/2.0/token
thomas.ai.create-url=https://aip.baidubce.com/rpc/2.0/aasr/v1/create
thomas.ai.query-url=https://aip.baidubce.com/rpc/2.0/aasr/v1/query
本项目将语音转录功能封装在 SpeechService服务中。
在调用任何功能之前,需要先基于上述apikey等,获取access token,同时也可以将token缓存起来:
@Cacheable(value = "thomas-ai-token")
public Optional<String> getAccessToken() {
Map<String, String> params = new HashMap<>(2);
params.put("client_id", API_KEY);
params.put("client_secret", SECRET_KEY);
//token请求URL
String requestUrl = ACCESS_TOKEN_URL
+ "?grant_type=client_credentials"
+ "&client_id={client_id}"
+ "&client_secret={client_secret}";
String jsonStr = restTemplate.getForObject(requestUrl, String.class, params);
JSONObject jsonObject = JSON.parseObject(jsonStr);
return Optional.ofNullable(jsonObject.getString("access_token"));
}
为方便后续调用,封装了一个通用的doPost方法:
public Optional<ResponseEntity<String>> doPost(String url, boolean needToken, Map<String, Object> values) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//将请求参数转换为json
String requestJson = JSON.toJSONString(values);
HttpEntity<String> request = new HttpEntity<>(requestJson, headers);
StringBuilder postUrl = new StringBuilder(url);
//需要追加token
if (needToken) {
Optional<String> opToken = getAccessToken();
if (opToken.isPresent()) {
//token存在则追加token
postUrl.append("?access_token=" + opToken.get());
} else {
log.error("没有获取到ACCESS TOKEN", opToken);
return Optional.empty();
}
}
return Optional.ofNullable(restTemplate.postForEntity(postUrl.toString(), request, String.class));
}
基于录音文件URL,创建文本转写任务:
//调用模式参见 https://ai.baidu.com/ai-doc/SPEECH/ck5diijkt
Map<String, Object> values = new HashMap<>(4);
values.put("speech_url", speechUrl);
values.put("format", "pcm");
values.put("pid", 1537);
values.put("rate", 16000);
return this.doPost(CREATE_URL, true, values);
提交任务后,API返回的是taskId,该id必须保存,因为后续需要基于该id查询转写结果:
//解析返回结果中的taskid,能解析到即代表提交成功
String taskId = JSON.parseObject(responseEntity.get().getBody()).getString("task_id");
将解析得到的id,保存到数据库中(本项目是基于JPA来进行数据库操作):
SpeechTaskInfo taskInfo = SpeechTaskInfo.builder()
.taskId(taskId)
.taskStatus(SpeechTaskStatus.Running)
.pcmKey(f.getKey())
.pcmUrl(url)
.build();
taskInfoRepo.save(taskInfo);
录音转写任务提交成功,最后一步就是等待离线任务运行完成,任务状态划分如下:
/** 转写中 */
Running,
/** 转写成功 */
Success,
/** 转写失败 */
Failure
在SpeechService中,封装了updateTaskResults方法,实现对任务的查询,并将转写成功的记录,记录到数据库中:
批量查询转录结果的调用非常简单:
// 技术文档 https://ai.baidu.com/ai-doc/SPEECH/6k5dilahb
Map<String, Object> values = new HashMap<>(1);
values.put("task_ids", taskIds);
return this.doPost(QUERY_URL, true, values);
处理API返回结果时,我们是采用的阿里巴巴的fastjson,实现将api返还的json对象,转换为java对象:
SpeechLogInfo logInfo = JSON.parseObject(responseEntity.get().getBody(), SpeechLogInfo.class);
// 分析每个解析任务的运行状态
logInfo.getTasks_info()
.stream()
.filter(infoBean -> infoBean.getTask_status().equals(SpeechTaskStatus.Success.name()))
.forEach(infoBean -> {
// 处理每个解析成功的任务
infoBean.getTask_result().getDetailed_result().forEach(r -> {
// 遍历每个解析结果,并存储到数据库中
SpeechTaskResult result = SpeechTaskResult.builder()
.taskId(infoBean.getTask_id())
.beginTime(r.getBegin_time() / 1000)
.endTime(r.getEnd_time() / 1000)
.words(String.join("", r.getRes()))
.build();
taskResultRepo.save(result);
});
// 更新任务为成功状态
Optional<SpeechTaskInfo> taskInfo = taskInfoRepo.findById(infoBean.getTask_id());
if (taskInfo.isPresent()) {
SpeechTaskInfo info = taskInfo.get();
//设置为成功状态
info.setTaskStatus(SpeechTaskStatus.Success);
//保存到数据库
taskInfoRepo.save(info);
count.incrementAndGet();
}
});
补充说明下,推荐在idea中安装GsonFormat插件,实现基于json格式字符串,快速创建java对象SpeechLogInfo。
到此,我们将完成了将PCM文件上传到云端,并实现调用录音转写服务,解析得到文本内容,如果相关问题或疑问,欢迎给我留言。最后一篇,我们将实现读取数据库的转录结果,导出为一个完整的word文档,方便阅读和分享。