結婚十年遊西湖
過春風十里,盡薺麥青青。春天總是讓人舒坦,而今年的三月,也因爲與媳婦結婚十年,顯得格外不同。兩人奢侈的請了一天假,瞞着孩子,重遊西湖,去尋找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;又搞了一個簡單的小程序...一頓操作之後,終於上線了。有興趣的同學可以掃碼體驗下。
小程序名稱 :文字轉語音實用工具;
小程序二維碼 : 不讓放二維碼,來一個鏈接快速體驗