결혼 10년 만에 서호 여행
봄바람이 지나고 10마일이 지나면 목자들과 밀이 모두 푸르다. 봄은 늘 마음을 편안하게 해주지만, 올해 3월은 유난히 남다르다. 아내와 결혼한 지 10년이 되었기 때문이다. 두 사람은 아이들을 숨겨 호화로운 휴가를 보내고, 13년 전의 아이스크림 가게를 찾기 위해 다시 서호를 찾았다(당시 동료였던 그녀에게 가장 비싼 아이스크림인 8위안을 사줬다). ) 그리고 13년 전에 팔았던 아이스크림 가게를 찾으러 13년 전 내가 앉았던 바로 그 의자에 팥 열쇠고리 삼촌(녹두 열쇠고리를 주셨네요 - 순수한 우정)이 가셨는데.. .. 낭만적인 추억에 잠겨 있던 중, 오랫동안 연락하지 않았던 친구가 갑자기 안지 다주하이에서 만난다는 소식을 들었다. 고향에서는 집 앞 뒤편에 대나무가 있으면 참 평화롭다고 생각했는데, 알고 보니 산과 들 곳곳에 있는 대나무도 독특한 맛을 갖고 있었습니다. 한 무리의 아이들이 잔디밭에서 축구를 하고 있습니다. 보세요, 아이들이 정말 재미있게 놀고 있습니다.
호기심은 지금부터 시작된다
채팅을 하다가 친구가 Qingdou라는 작은 프로그램을 보여줬는데, 영상 카피 추출 기능이 매력적이었어요. Douyin 또는 Kuaishou와 같은 짧은 비디오에 대한 링크를 복사하면 비디오 사본을 추출할 수 있습니다. 호기심에 이끌려 탐험의 여정이 시작되었습니다. 시작하기는 쉽지만 놓기는 어려울 것이라고는 결코 생각하지 못했습니다.
간단한 생각 끝에 일반적인 프로세스가 결정되었으며 이는 세 단계로 나뉩니다.
비디오 파일 추출 -> 오디오 분리 -> 오디오를 텍스트로. 그러다가 즐겁게 코딩을 시작했어요. 현실은 곧 나를 강타했고, 그것은 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() 오류: 스트림 정보를 찾을 수 없습니다.
2. 브라우저도 파일을 저장하지 못했습니다.
3. Thunder 다운로드 및 구문 분석도 실패했습니다.
...
제3자 인터페이스에서 반환된 비디오 인코딩에 문제가 있다는 의심이 들기 시작했습니다. Douyin 저장 파일이 성공적으로 구문 분석되었을 때 저의 의심은 더욱 확증되었습니다. 하지만 WeChat 애플릿 saveVideoToPhotosAlbum을 사용하여 저장한 파일은 성공적으로 구문 분석할 수 있습니다... 나는 의심하기 시작했습니다. 그래서 다양한 매개변수가 무작위로 조정되기 시작했습니다. 수많은 실패 끝에 나는 대담한 아이디어를 떠올렸습니다. 내가 다운로드한 것을 파싱할 수 없다면 언제든지 직접 다운로드한 javaCV를 파싱할 수 있습니다. 물론이죠. 위 코드는 한 줄을 수정했습니다.
//FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getVideoInputStream());
// 直接传url
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getUrl());
다음 단계는 추출된 오디오 파일을 기반으로 Tencent Cloud의 ars 인터페이스를 호출하는 것입니다. 이전에 내부 금융 로봇을 구현하기 위해 Openai의 인터페이스를 사용할 때 음성 입력을 텍스트로 변환하는 인터페이스를 작성했는데 그냥 가져가서 넣으면 괜찮았습니다. 한 문장 인터페이스는 다음과 같이 호출됩니다. 1분 이상 소요될 경우에는 긴 음성 인터페이스를 호출하면 됩니다. (참고: 한 문장 인터페이스는 동기적으로 반환되고 긴 음성은 비동기 콜백입니다.)
/**
* @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를 만드는 것을 도와주었고 저는 간단한 작은 프로그램을 만들었습니다... 얼마 후, 그것은 마침내 온라인이 되었습니다. 관심 있는 학생들은 코드를 스캔하여 체험해 볼 수 있습니다.
미니 프로그램 이름: 텍스트 음성 변환 유틸리티;
미니 프로그램 QR 코드: QR 코드는 허용되지 않습니다. 빠른 경험을 위한 링크는 다음과 같습니다.
Linus는 커널 개발자가 탭을 공백으로 대체하는 것을 막기 위해 스스로 노력했습니다. 그의 아버지는 코드를 작성할 수 있는 몇 안되는 리더 중 한 명이고, 둘째 아들은 오픈 소스 기술 부서의 책임자이며, 막내 아들은 오픈 소스 코어입니다. 기고자 Robin Li: 자연 언어 는 새로운 범용 프로그래밍 언어가 될 것입니다. 오픈 소스 모델은 Huawei에 비해 점점 더 뒤쳐질 것입니다 . 일반적으로 사용되는 5,000개의 모바일 애플리케이션을 Hongmeng으로 완전히 마이그레이션하는 데 1년이 걸릴 것입니다. 타사 취약점. 기능, 안정성 및 개발자의 경험이 크게 개선된 Quill 2.0 이 출시되었습니다. Ma Huateng과 Zhou Hongyi는 "원한을 제거하기 위해" 공식적으로 출시되었습니다. Laoxiangji의 소스는 코드가 아닙니다. Google이 대규모 구조 조정을 발표한 이유는 매우 훈훈합니다.