短影片文案提取的簡單實現

2J發表於2024-03-29

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

小程式名稱 :智慧配音實用工具;

小程式二維碼 :


相關文章