0

短视频文案提取的简单实现 - 2J

 1 month ago
source link: https://www.cnblogs.com/jijunjian/p/18103228
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

       过春风十里,尽荠麦青青。春天总是让人舒坦,而今年的三月,也因为与媳妇结婚十年,显得格外不同。两人奢侈的请了一天假,瞒着孩子,重游西湖,去寻找13年前的冰棍店(给当时还是同事的她买了最贵的一个雪糕-8元),去寻找13年前卖红豆钥匙扣的大爷(她送我了一个绿豆的钥匙扣-纯洁的友谊),去坐一坐13年前坐过的那条凳子... 正当沉浸在浪漫的回忆中时,一个许久未曾联系的好友,突然来了消息,相约安吉大竹海。以前觉得老家的房前屋后都是竹子已是清幽之至,原来漫山遍野的竹子亦是别有一番风味。一群娃在草地上尽情的踢球,瞧,娃玩得多开心。

88102-20240329101900210-1853163049.png

闲聊之余,好友展示一个叫轻抖的小程序,里面一个视频文案提取的功能吸引了我。随便复制一条抖音,快手之类的短视频的链接就可以提取视频的文案。好奇心驱使之下,开始了一段探索之路。没曾想,开始容易,放下难。

经过一番简单的思索确定了大概流程,分三个步骤:

提取视频文件 -> 音频分离 -> 音频转文字。而后就兴高采烈的编码起来了。很快现实就给当头一棒,应验了那句伴随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;
    }
88102-20240329102037395-938578229.gif

然后使用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;
    }
88102-20240329102037395-938578229.gif

写到这里时,我以为胜利就如东方红霞之下呼之欲出的红日,已然无限接近,测试一个用例完美,二个用例完美,正当准备进行一个语音转文字的阶段时,最后一个单测失败。为此,开始了一轮旷日持久的调试路。

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());
88102-20240329102037395-938578229.gif

接下来就是根据提取的音频文件,调用腾讯云的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(), "语音转文字异常,请重试");
        }
    }
88102-20240329102037395-938578229.gif

长语音转文本也差不多。代码如下

    /**
     * @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;
    }
88102-20240329102037395-938578229.gif

一开始只是凭着对文案提取好奇,没曾想,一写就停不下来;后端实现了,如果没有一个前端的呈现又感觉略有遗憾;于是又让媳妇帮忙搞了一套UI;又搞了一个简单的小程序...一顿操作之后,终于上线了。有兴趣的同学可以扫码体验下。

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

小程序二维码 : 

88102-20240329101934088-197125357.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK