Extrair pequenas cópias de vídeo é tão fácil

Viajando para West Lake após dez anos de casamento

       Dezesseis quilômetros depois da brisa da primavera, todos os pastores e o trigo estão verdes. A primavera sempre deixa as pessoas confortáveis, e este mês de março é particularmente diferente porque estou casado com minha esposa há dez anos. Os dois tiraram um dia de folga luxuoso, escondendo os filhos, e revisitaram o Lago Oeste para encontrar a loja de picolés de 13 anos atrás (eles compraram para ela o sorvete mais caro - 8 yuans - para ela que era colega na época ), e para encontrar a sorveteria que vendia há 13 anos. O tio do chaveiro de feijão vermelho (ela me deu um chaveiro de feijão mungo - pura amizade), foi sentar no mesmo banquinho em que eu sentei há 13 anos. .No momento em que eu estava imerso em lembranças românticas, uma pessoa com quem eu não contatava há muito tempo apareceu de repente. Um amigo meu recebeu de repente a notícia de que nos encontraríamos em Dazhuhai, Anji. Eu costumava pensar que era muito tranquilo ter bambus na frente e nos fundos da minha casa na minha cidade natal. Acontece que os bambus espalhados pelas montanhas e planícies também têm um sabor único. Um grupo de crianças está jogando futebol na grama. Olha, as crianças estão se divertindo muito.

A curiosidade começa a partir de agora

Enquanto conversava, um amigo me mostrou um pequeno programa chamado Qingdou. A função de extrair cópias de vídeo me atraiu. Basta copiar um link para um vídeo curto como Douyin ou Kuaishou para extrair a cópia do vídeo. Movidos pela curiosidade, iniciamos uma jornada de exploração. Nunca pensei que seria fácil começar, mas difícil deixar ir.

Após algumas reflexões simples, foi determinado o processo geral, que é dividido em três etapas:

Extraia arquivos de vídeo -> Separação de áudio -> Áudio para texto. Então comecei a codificar feliz. A realidade logo me atingiu com força e se tornou realidade no velho provérbio de Sichuan que está comigo há 30 anos: para dizer o mínimo, é como um raio de luz (o dialeto de Sichuan parece interessante quando você o lê). A primeira dificuldade é: como baixar vídeos com base em links compartilhados e suportar diversas plataformas comuns. Depois de tentar um pouco, desisti. Afinal, “não queria fazer isso”. Mais tarde, descobri acidentalmente que existem muitas plataformas desse tipo que fornecem interfaces para download de vídeos baseadas em URLs, então apenas usei o. interface de terceiros.

Com o link do vídeo, é simples baixá-lo localmente (porém, pode haver armadilhas em locais simples), inserir diretamente o código e retornar o InputStream gerado pelo arquivo.

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;
    }

Em seguida, use javacv para separar o áudio. Não há nada de especial nisso. O áudio separado é coletado por meio do FFmpegFrameRecorder. Também carregue diretamente o código.

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;
    }

tempestade antes do amanhecer

Quando escrevi isso, pensei que a vitória era como o sol vermelho que estava prestes a emergir sob as nuvens vermelhas no leste. Já estava infinitamente próximo. estava se preparando para um estágio de fala para texto, o último teste falhou. Para tanto, iniciou-se uma prolongada rodada de depuração.

1. baixar e salvar arquivo http - falha na análise - erro avformat_find_stream_info(): não foi possível encontrar informações de fluxo;

2. O navegador também não conseguiu salvar o arquivo;

3. O download e a análise do Thunder também falharam;

...

Comecei a suspeitar que há um problema com a codificação de vídeo retornada pela interface de terceiros; quando o arquivo salvo do Douyin foi analisado com sucesso, minha suspeita foi posteriormente confirmada; Mas os arquivos salvos usando o miniaplicativo saveVideoToPhotosAlbum do WeChat podem ser analisados ​​com sucesso...Comecei a duvidar de mim mesmo. Assim, vários parâmetros começaram a ser ajustados aleatoriamente. Depois de inúmeras falhas, tive uma ideia ousada. Se você não conseguir analisar o que baixei, poderá analisar o javaCV que baixou. Com certeza. O código acima modificou uma linha.


//FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getVideoInputStream());
// 直接传url 
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(req.getUrl());

A próxima etapa é chamar a interface ars do Tencent Cloud com base nos arquivos de áudio extraídos. Quando usei a interface da Openai para implementar um robô financeiro interno, escrevi uma interface para converter entrada de voz em texto, apenas peguei e coloquei e ficou tudo bem. A interface de uma frase é chamada da seguinte maneira. Se demorar mais de um minuto, basta ligar para a interface de voz longa. (Observação: a interface de uma frase retorna de forma síncrona e a voz longa é um retorno de chamada assíncrono)

    /**
     * @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(), "语音转文字异常,请重试");
        }
    }

Discurso longo para texto é semelhante. código mostrado abaixo

    /**
     * @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;
    }

fim sem fim

No início, eu estava apenas curioso sobre a extração de direitos autorais, mas nunca pensei que não conseguiria parar de escrevê-lo. O backend foi implementado e fiquei um pouco arrependido se não houvesse apresentação de front-end. esposa para me ajudar a construir uma UI; e eu fiz um pequeno programa simples... depois de um tempo, ele finalmente estava online. Os alunos interessados ​​​​podem escanear o código para experimentá-lo.

Nome do miniprograma: Utilitário de conversão de texto em fala;

Código QR do miniprograma:  Nenhum código QR é permitido, aqui está um link para uma experiência rápida

 

 

 

Linus assumiu a responsabilidade de evitar que os desenvolvedores do kernel substituíssem tabulações por espaços. Seu pai é um dos poucos líderes que sabe escrever código, seu segundo filho é o diretor do departamento de tecnologia de código aberto e seu filho mais novo é um núcleo de código aberto. contribuidor Robin Li: A linguagem natural se tornará uma nova linguagem de programação universal. O modelo de código aberto ficará cada vez mais atrás da Huawei: levará 1 ano para migrar totalmente 5.000 aplicativos móveis comumente usados ​​para Hongmeng. vulnerabilidades de terceiros. O editor de rich text Quill 2.0 foi lançado com recursos, confiabilidade e desenvolvedores. A experiência foi bastante melhorada. fonte de Laoxiangji não é o código, as razões por trás disso são muito comoventes. O Google anunciou uma reestruturação em grande escala.
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/jijunjian/blog/11054025
Recomendado
Clasificación