短視頻文案提取的簡單實現

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

小程序名稱 :智能配音實用工具;

小程序二維碼 : 

 

 

 

 

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章