• 短视频文案提取的简单实现


           过春风十里,尽荠麦青青。春天总是让人舒坦,而今年的三月,也因为与媳妇结婚十年,显得格外不同。两人奢侈的请了一天假,瞒着孩子,重游西湖,去寻找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;又搞了一个简单的小程序...一顿操作之后,终于上线了。有兴趣的同学可以扫码体验下。

    小程序名称 :智能配音实用工具;

    小程序二维码 : 

     

     

     

     

     


  • 相关阅读:
    rabbitMQ集群搭建
    基于PHP的店家服务与管理交互平台
    怎么对遍历数组循环请求接口
    【广州华锐互动】数字孪生智慧楼宇3D可视化系统:掌握实时运行状态,优化运营管理
    内存泄漏Memory leak
    DRM系列(4)之drmModePageFlip
    sklearn基础篇(六)-- 决策树(decision tree)
    应急响应常用命令(Linux)---读书笔记
    HTML之背景颜色、图片、超链接
    webpack与vue-cli的关系
  • 原文地址:https://www.cnblogs.com/jijunjian/p/18103228