过春风十里,尽荠麦青青。春天总是让人舒坦,而今年的三月,也因为与媳妇结婚十年,显得格外不同。两人奢侈的请了一天假,瞒着孩子,重游西湖,去寻找13年前的冰棍店(给当时还是同事的她买了最贵的一个雪糕-8元),去寻找13年前卖红豆钥匙扣的大爷(她送我了一个绿豆的钥匙扣-纯洁的友谊),去坐一坐13年前坐过的那条凳子... 正当沉浸在浪漫的回忆中时,一个许久未曾联系的好友,突然来了消息,相约安吉大竹海。以前觉得老家的房前屋后都是竹子已是清幽之至,原来漫山遍野的竹子亦是别有一番风味。一群娃在草地上尽情的踢球,瞧,娃玩得多开心。
过春风十里,尽荠麦青青。春天总是让人舒坦,而今年的三月,也因为与媳妇结婚十年,显得格外不同。两人奢侈的请了一天假,瞒着孩子,重游西湖,去寻找13年前的冰棍店(给当时还是同事的她买了最贵的一个雪糕-8元),去寻找13年前卖红豆钥匙扣的大爷(她送我了一个绿豆的钥匙扣-纯洁的友谊),去坐一坐13年前坐过的那条凳子... 正当沉浸在浪漫的回忆中时,一个许久未曾联系的好友,突然来了消息,相约安吉大竹海。以前觉得老家的房前屋后都是竹子已是清幽之至,原来漫山遍野的竹子亦是别有一番风味。一群娃在草地上尽情的踢球,瞧,娃玩得多开心。
闲聊之余,好友展示一个叫轻抖的小程序,里面一个视频文案提取的功能吸引了我。随便复制一条抖音,快手之类的短视频的链接就可以提取视频的文案。好奇心驱使之下,开始了一段探索之路。没曾想,开始容易,放下难。
经过一番简单的思索确定了大概流程,分三个步骤:
提取视频文件 -> 音频分离 -> 音频转文字。而后就兴高采烈的编码起来了。很快现实就给当头一棒,应验了那句伴随30年的四川老谚语:说得轻巧,是根灯草(四川话念来就有味儿了)。第一个难点就是:如何根据分享的链接下载视频,还能支持各种通用平台。尝试好一会儿后放弃了,毕竟”志不在此“嘛,后来偶然发现有不少这样的平台,专门提供根据url 下载视频的接口,就直接用三方的接口了。
有了视频链接,下载到本地就简单了(然则,简单的地方可能会有坑),直接上代码,返回文件生成的InputStream。
public InputStream run(MediaDownloadReq req) {
//根据url获取视频流
InputStream videoInputStream = null;
try {
String newName = "video-"+String.format("%s-%s", System.currentTimeMillis(), UUID.randomUUID().toString())+"."+req.getTargetFileSuffix();
File folder = new File(tempPath);
if (!folder.exists()) {
folder.mkdir();
}
File file = HttpUtil.downloadFileFromUrl(req.getUrl(), new File(tempPath +"" + newName+""), new StreamProgress() {
// 开始下载
@Override
public void start() {
log.info("Start download file...");
}
// 每隔 10% 记录一次日志
@Override
public void progress(long total) {
//log.info("Download file progress: {} ", total);
}
@Override
public void finish() {
log.info("Download file success!");
}
});
videoInputStream = new FileInputStream(file);
file.delete();
} catch (Exception e) {
log.error("获取视频流失败 req ={}", req.getUrl(), e);
throw new BusinessException(ErrorCodeEnum.DOWNLOAD_VIDEO_ERROR.code(), "获取视频流失败");
}
return videoInputStream;
}
然后使用javacv 分离音频,这个没什么特别的地方, 通过FFmpegFrameRecorder 搜集分离的音频。也直接上代码。
public ExtractAudioRes run(ExtractAudioReq req) throws Exception {
long current = System.currentTimeMillis();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//音频记录器,extractAudio:表示文件路径,2:表示两声道
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputStream, 2);
recorder.setAudioOption("crf", "0");
recorder.setAudioQuality(0);
//比特率
recorder.setAudioBitrate(256000);
//采样率
//recorder.setSampleRate(16000);
recorder.setSampleRate(8000);
recorder.setFormat(req.getAudioFormat());
//音频编解码
recorder.setAudioCodec(avcodec.AV_CODEC_ID_PCM_S16LE);
//开始记录
recorder.start();
//读取视频信息
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getVideoInputStream());
grabber.setSampleRate(8000);
//FFmpegLogCallback.set(); 调试日志
// 设置采集器构造超时时间(单位微秒,1秒=1000000微秒)
grabber.setOption("stimeout", String.valueOf(TimeUnit.MINUTES.toMicros(30L)));
grabber.start();
recorder.setAudioChannels(grabber.getAudioChannels());
Frame f;
Long audioTime = grabber.getLengthInTime() / 1000/ 1000;
current = System.currentTimeMillis();
//获取音频样本,并且用recorder记录
while ((f = grabber.grabSamples()) != null) {
recorder.record(f);
}
grabber.stop();
recorder.close();
ExtractAudioRes extractAudioRes = new ExtractAudioRes(outputStream.toByteArray(), audioTime, outputStream.size() /1024);
extractAudioRes.setFormat(req.getAudioFormat());
return extractAudioRes;
}
写到这里时,我以为胜利就如东方红霞之下呼之欲出的红日,已然无限接近,测试一个用例完美,二个用例完美,正当准备进行一个语音转文字的阶段时,最后一个单测失败。为此,开始了一轮旷日持久的调试路。
1, http下载保存文件-解析失败- avformat_find_stream_info() error : Could not find stream information;
2.浏览器保存文件也失败;
3, 迅雷下载解析也失败;
...
我已经开始怀疑三方接口返回的视频编码有问题了;当抖音保存文件解析成功时,更加印证了我的怀疑。但是使用微信小程序 saveVideoToPhotosAlbum 保存的文件居然可以解析成功...我开始怀疑自己了。于是各种参数开始胡乱一通调整。失败了无数次后,有了一个大胆的想法,我下载的你不能解析,那javaCV你自己下载的你总能解析了吧。 果然如此。上面的代码就修改了一行。
//FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getVideoInputStream());
// 直接传url
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getUrl());
接下来就是根据提取的音频文件,调用腾讯云的ars 接口。之前使用Openai 的接口实现内部财务机器人时,有写过通过语音输入转文字的接口,直接拿过来放上就ok了。 一句话接口调用如下,如果是超过一分钟的,调用长语音接口就可以了。(注:一句话接口同步返回,长语音是异步回调)
/**
* @param audioRecognitionReq
* @description: 语音转文字
* @author: jijunjian
* @date: 11/21/23 09:48
* @param: [bytes]
* @return: java.lang.String
*/
@Override
public String run(AudioRecognitionReq audioRecognitionReq) {
log.info("一句话语音语音转文字开始");
AsrClient client = new AsrClient(cred, "");
SentenceRecognitionRequest req = new SentenceRecognitionRequest();
req.setSourceType(1L);
req.setVoiceFormat(audioRecognitionReq.getFormat());
req.setEngSerViceType("16k_zh");
String base64Encrypted = BaseEncoding.base64().encode(audioRecognitionReq.getBytes());
req.setData(base64Encrypted);
req.setDataLen(Integer.valueOf(audioRecognitionReq.getBytes().length).longValue());
String text = "";
try {
SentenceRecognitionResponse resp = client.SentenceRecognition(req);
log.info("语音转文字结果:{}", JSONUtil.toJsonStr(resp));
text = resp.getResult();
if (Strings.isNotBlank(text)){
return text;
}
return "无内容";
} catch (TencentCloudSDKException e) {
log.error("语音转文字失败:{}",e);
throw new BusinessException(AUDIO_RECOGNIZE_ERROR.code(), "语音转文字异常,请重试");
}
}
长语音转文本也差不多。代码如下
/**
* @param audioRecognitionReq
* @description: 语音转文字
* @author: jijunjian
* @date: 11/21/23 09:48
* @param: [bytes]
* @return: java.lang.String
*/
@Override
public String run(AudioRecognitionReq audioRecognitionReq) {
log.info("极速语音转文字开始");
Credential credential = Credential.builder().secretId(AppConstant.Tencent.asrSecretId).secretKey(AppConstant.Tencent.asrSecretKey).build();
String text = "";
try {
FlashRecognizer recognizer = SpeechClient.newFlashRecognizer(AppConstant.Tencent.arsAppId, credential);
byte[] data = null;
if (audioRecognitionReq.getBytes() != null){
data = audioRecognitionReq.getBytes();
}else {
//根据文件路径获取识别语音数据 以后再实现
}
//传入识别语音数据同步获取结果
FlashRecognitionRequest recognitionRequest = FlashRecognitionRequest.initialize();
recognitionRequest.setEngineType("16k_zh");
recognitionRequest.setFirstChannelOnly(1);
recognitionRequest.setVoiceFormat(audioRecognitionReq.getFormat());
recognitionRequest.setSpeakerDiarization(0);
recognitionRequest.setFilterDirty(0);
recognitionRequest.setFilterModal(0);
recognitionRequest.setFilterPunc(0);
recognitionRequest.setConvertNumMode(1);
recognitionRequest.setWordInfo(1);
FlashRecognitionResponse response = recognizer.recognize(recognitionRequest, data);
if (SuccessCode.equals(response.getCode())){
text = response.getFlashResult().get(0).getText();
return text;
}
log.info("极速语音转文字失败:{}", JSONUtil.toJsonStr(response));
throw new BusinessException(AUDIO_RECOGNIZE_ERROR.code(), "极速语音转换失败,请重试");
} catch (Exception e) {
log.error("语音转文字失败:{}",e);
throw new BusinessException(AUDIO_RECOGNIZE_ERROR.code(), "极速语音转文字异常,请重试");
}
}
/**
* @param req
* @description: filter 根据参数选
* @author: jijunjian
* @date: 3/3/24 18:54
* @param:
* @return:
*/
@Override
public Boolean filter(AudioRecognitionReq req) {
if (req.getAudioTime() == null || req.getAudioTime() >= AppConstant.Tencent.Max_Audio_Len || req.getAudioSize() >= AppConstant.Tencent.Max_Audio_Size){
return true;
}
return false;
}
一开始只是凭着对文案提取好奇,没曾想,一写就停不下来;后端实现了,如果没有一个前端的呈现又感觉略有遗憾;于是又让媳妇帮忙搞了一套UI;又搞了一个简单的小程序...一顿操作之后,终于上线了。有兴趣的同学可以扫码体验下。
小程序名称 :智能配音实用工具;
小程序二维码 :