Tentei usar o whisper live via microfone

Criei esse arquivo pra ir anotando minhas tentativas de rodar o whisper em tempo real, puxando o áudio direto do microfone. Já vou deixar claro que isso não funcionou do jeito que eu esperava, e ainda tem coisa pra testar.

Na minha cabeça, talvez o certo fosse trabalhar direto com o modelo do whisper, sem usar o código oficial da OpenAI. Aquela janela fixa de 30 segundos pode ter atrapalhado real. Pode até ser que precise refinar o modelo pra rodar liso num esquema live.

Mas enfim… isso é só uma ideia. Não fui muito além do que tá descrito aqui porque quero ver primeiro se esse tipo de conteúdo realmente chama atenção do pessoal pra eu focar mais tempo pra gerar conteúdo (vídeos, posts, aulas, etc).

Imagem de um robô triste

(🚫 FAILED) Captura do microfone via ffmpeg

Minha primeira tentativa foi capturar o áudio do computador usando o ffmpeg.

Pra deixar claro: tô usando o ChatGPT e o Gemini pra me ajudar nas partes que eu manjo menos, principalmente as coisas mais técnicas de áudio. Além disso, tô rodando tudo com esse setup aqui (não sei se faz diferença, mas vai que…):

  • MacBook Pro 2021
  • Chip Apple M1 Max
  • 32 GB de RAM
  • SSD de 1 TB
  • macOS Sequoia 15.5

Listando os dispositivos de áudio

Obs: tô testando só no macOS por enquanto.

# macOS
ffmpeg -hide_banner -f avfoundation -list_devices true -i ""

# Linux
ffmpeg -hide_banner -f alsa -list_devices true -i ""
# ou (às vezes mais confiável)
arecord -l
# ou
arecord --list-devices

# Windows
ffmpeg -hide_banner -list_devices true -f dshow -i dummy

No meu caso, a saída foi mais ou menos como a que mostro logo abaixo. Só dei uma enxugada, tirando os dispositivos que não interessavam (tipo fone, celular, etc). Também cortei aquele trecho inicial que o ffmpeg cospe sempre.

➜  Desktop
ffmpeg -hide_banner -f avfoundation -list_devices true -i "" 2>&1

2025-06-18 09:15:21.567 ffmpeg[37312:1839628] WARNING:
Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.

2025-06-18 09:15:21.708 ffmpeg[37312:1839628] WARNING:
AVCaptureDeviceTypeExternal is deprecated for Continuity Cameras.

Please use AVCaptureDeviceTypeContinuityCamera and add NSCameraUseContinuityCameraDeviceType to your Info.plist.

[AVFoundation indev @ 0x153706960] AVFoundation video devices:
[AVFoundation indev @ 0x153706960] [0] Câmera FaceTime HD
[AVFoundation indev @ 0x153706960] [5] Capture screen 0
[AVFoundation indev @ 0x153706960] [6] Capture screen 1
[AVFoundation indev @ 0x153706960] AVFoundation audio devices:
[AVFoundation indev @ 0x153706960] [0] MOTIV Mix Virtual
[AVFoundation indev @ 0x153706960] [1] JBL TUNE125BT FOREBA
[AVFoundation indev @ 0x153706960] [2] Microfone (MacBook Pro)

[in#0 @ 0x600001f5c600] Error opening input: Input/output error

Error opening input file .
Error opening input files: Input/output error

A parte importante dessa saída é saber duas coisas: o [ID] e o nome do dispositivo.

Por exemplo, se eu quiser usar o Microfone (MacBook Pro), isso quer dizer que o ID do dispositivo de áudio que eu vou usar é o 2.


(🚫 FAILED) Testando a captura de áudio em um .wav

Antes de tudo, bora garantir que tá tudo funcionando bonitinho. A ideia aqui é só gravar um .wav com o áudio capturado direto do microfone. Ainda não é o comando final, é só pra testar mesmo.

# Lembra do meu [2] Microfone (MacBook Pro) -> ID 2
# No comando abaixo, o ":2" representa o dispositivo que eu tô usando
# Resultado: grava um arquivo out.wav com 10 segundos de áudio
ffmpeg -f avfoundation -i ":2" -t 10 -ac 1 -ar 16000 out.wav

Ou seja, pra capturar só o [2] Microfone (MacBook Pro), é só passar :2 no argumento -i.

Se tu quiser capturar vídeo e áudio ao mesmo tempo, o formato vira V:A, onde V é o ID do vídeo e A o do áudio.

Tipo assim:

# Captura da Câmera FaceTime HD (ID 0) + Microfone (MacBook Pro) (ID 2)
# Grava um out.mp4 com 10 segundos de vídeo e áudio dos dispositivos 0 e 2
ffmpeg -f avfoundation -framerate 30 -i "0:2" -t 10 out.mp4

(🚫 FAILED) O que rolou com o ffmpeg?

No meu caso, o ffmpeg não funcionou do jeito que eu queria. O áudio ficou todo picotado, com umas falhas bem esquisitas. Daí pra frente, passei praticamente o dia inteiro testando várias coisas: mudei o sample rate, tentei outros codecs, troquei de microfone, testei outras fontes (até tentei capturar o som direto do sistema). Nada deu certo.

Minha conclusão: o avfoundation no meu macOS não tá se comportando direito.

Testei comandos como esse aqui:

ffmpeg \
    -thread_queue_size 512 \
    -loglevel debug \
    -f avfoundation \
    -i ":2" \
    -c:a copy \
    -t 10 \
    out.wav \
    -y

Esse -c:a copy (pra manter o codec de áudio original) foi uma das últimas tentativas, depois de trocar um monte de outras coisas. E com isso o ffmpeg revelou que o áudio tava vindo com essas specs:

Stream #0:0, 1, 1/1000000: Audio: pcm_f32le, 48000 Hz, mono, flt, 1536 kb/s

Eu já tinha testado pcm_f32le com 48000 Hz antes, então… nenhuma surpresa.

Enfim, deixo isso aqui documentado porque, se acontecer contigo também, já te poupo umas horas de frustração. A única coisa que resolveu o problema de áudio tremido/picotado foi usar o sox.


(✅ SUCCESS) sox pra capturar o áudio

Na primeira tentativa com sox, já rolou de boa, zero esforço.

sox \
    -b 32 \
    -e float \
    -r 16000 \
    -c 1 \
    -d \
    --buffer $((16000*4*10)) \
    out.wav \
    trim 0 10 \
    fade t 1 -0 1

Esse comando foi gerado pelo Gemini ou ChatGPT (não lembro). Só pedi um exemplo que gravasse 10 segundos de áudio com fade-in e fade-out, só pra teste mesmo.

Depois fui testando outras paradas, mas o comando que pretendo usar com o whisper é esse aqui:

# 🚫 não executa ainda
sox -t \
    coreaudio "Microfone (MacBook Pro)" \
    -b 16 \
    -e signed-integer \
    -r 16000 -c 1 \
    -t raw \
    -

Repara que a saída tá indo pro stdout (por isso tem esse - no fim), não pra um arquivo. Ou seja, isso aqui é só a base pra integrar com outro processo, tipo jogar direto no whisper.

Eu não entendo muito do sox ainda (primeiro uso), mas usei o ffmpeg (lá em cima) pra listar os dispositivos. E aqui é diferente: em vez de usar o ID como no ffmpeg, tu passa o nome do dispositivo, tipo, meu caso: "Microfone (MacBook Pro)".


(🤔 SUCCESS?) whisper live, ou quase

Aqui é onde começo a achar que talvez eu precise refinar o modelo ou até usar ele “cru”, sem passar pelo código oficial da OpenAI (whisper).

Fiz um código bem porquinho, só pra ver se ia funcionar. A ideia era: se desse certo, aí sim eu refinava tudo com calma.

Considera isso aqui como um MVP na versão 0.0.0alpha mesmo.

# pyright: basic
import asyncio

import numpy as np
import whisper
from scipy.io import wavfile

MODEL = whisper.load_model("turbo")


async def recorder(queue, counter, buffer, chunk_size, sr=16000, duration=5):
    print("🗣️ Recording started")

    proc = await asyncio.create_subprocess_exec(
        "sox",
        "-t",
        "coreaudio",
        "Microfone (MacBook Pro)",
        "-b",
        "16",
        "-e",
        "signed-integer",
        "-r",
        "16000",
        "-c",
        "1",
        "-t",
        "raw",
        "-",  # raw para facilitar o corte
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.DEVNULL,
    )

    while True:
        data = await proc.stdout.read(chunk_size)
        if not data:
            break
        buffer += data

        chunk_byte_size = sr * 2 * duration

        if len(buffer) >= chunk_byte_size:
            audio_chunk = buffer[:chunk_byte_size]
            buffer = buffer[chunk_byte_size:]

            audio_np = np.frombuffer(audio_chunk, np.int16).astype(np.float32) / 32768.0
            await queue.put(audio_np)

            wavfile.write(
                f"media/debug_{counter}.wav",
                16000,
                audio_np,
            )

            print(f"🗣️ Chunk {counter} ends \n")
            counter += 1


async def transcriber(queue):
    prefix = ""
    while True:
        print("💬 Whisper started")

        audio = await queue.get()
        audio = whisper.pad_or_trim(audio)

        mel = whisper.log_mel_spectrogram(audio, n_mels=MODEL.dims.n_mels).to(
            MODEL.device
        )

        options = whisper.DecodingOptions(
            language="en",
            temperature=0,
            beam_size=1,
            patience=0,
            without_timestamps=True,
            prompt=None,
            prefix=None,
        )
        result = whisper.decode(
            MODEL,
            mel,
            options,
        )

        prefix = result.text
        print("💬", result.text, "\n")
        queue.task_done()


async def main():
    from pathlib import Path
    from shutil import rmtree

    media = Path("media")
    rmtree(media)
    media.mkdir(exist_ok=True)

    queue = asyncio.Queue()
    counter = 0
    buffer = b""
    chunk_size = 4096

    await asyncio.gather(
        recorder(queue, counter, buffer, chunk_size, 16000, 30), transcriber(queue)
    )


if __name__ == "__main__":
    asyncio.run(main())

Nesse código eu uso asyncio justamente pra não travar tudo enquanto o whisper tá transcrevendo. A ideia é deixar o processo rodando em paralelo: enquanto uma parte grava, a outra já vai transcrevendo em tempo real.

Também uso o scipy.io.wavfile pra salvar os áudios em chunks, do mesmo jeitinho que o whisper recebe. Isso ajuda no debug: depois eu junto esses arquivos e escuto como ficou o áudio real que foi processado.

A função recorder é quem inicia o sox com o microfone que escolhi e roda um loop que fica monitorando a quantidade de bytes acumulados. Essa contagem é baseada no tamanho que cada trecho de áudio teria, considerando a duração que defini.

# Sample rate (16000) * Sample Width (2 bytes) * duração (ex: 5s)
chunk_byte_size = sr * 2 * duration

Quando o tamanho do buffer atinge esse chunk_byte_size, a gente corta exatamente até ali. Esses são os bytes crus do trecho de áudio que vai pra transcrição. Em seguida, o buffer é atualizado pra remover esse pedaço processado e guardar qualquer sobra, que vai ser usada no próximo ciclo.

Esse loop aqui embaixo é o coração da gravação. Ele fica rodando enquanto o sox tá mandando dados pela stdout.

while True:
    # captura o que vem do sox
    data = await proc.stdout.read(chunk_size)
    if not data:
        break
    # joga no buffer
    buffer += data

    # calcula quantos bytes correspondem à duration configurada
    chunk_byte_size = sr * 2 * duration

    # se o buffer já tiver o tamanho certo pra um trecho de áudio completo
    if len(buffer) >= chunk_byte_size:
        # pegamos só o pedaço que interessa
        audio_chunk = buffer[:chunk_byte_size]
        # e atualizamos o buffer, removendo o que já usamos
        buffer = buffer[chunk_byte_size:]

Aí, com esse audio_chunk em mãos, é só converter pra um formato que o whisper entenda e jogar isso na queue, que o transcriber vai processar depois:

audio_np = np.frombuffer(audio_chunk, np.int16).astype(np.float32) / 32768.0
await queue.put(audio_np)

Esse .frombuffer(...).astype(...) / 32768.0 basicamente transforma os bytes crus em um array de float32 com valores normalizados entre -1 e 1, que é o formato que o whisper espera.

Como eu queria ver exatamente o que o whisper também “tava enxergando”, salvei cada trecho de áudio assim:

wavfile.write(
    f"media/debug_{counter}.wav",
    16000,
    audio_np,
)

Usei esse counter só pra gerar arquivos separados, tipo: debug_0.wav, debug_1.wav, debug_2.wav, e por aí vai.

Depois juntei tudo com o ffmpeg:

ffmpeg -f concat -safe 0 -i input.txt -c copy out.wav

E o arquivo input.txt fica assim:

file 'debug_0.wav'
file 'debug_1.wav'
file 'debug_2.wav'

Até esse ponto, tava tudo fluindo lindamente. Eu já tava achando que ia dar bom. Mas… o resultado final foi bem mais ou menos.


(🤔 SUCCESS?) os problemas!

É aqui que entra o whisper de verdade. Testei um monte de coisas pra tentar deixar tudo o mais rápido possível, e, claro, sem perder precisão na transcrição.

Dá uma olhada nos comentários que deixei no código. Logo mais abaixo eu explico outras coisas que descobri no caminho.

Essa é a função transcriber, que fica rodando enquanto tem coisa na fila (queue). Comentei bastante no código, mas aqui vai uma visão geral com uns complementos:

async def transcriber(queue):
    prefix = ""  # USEI ISSO DE DUAS FORMAS DIFERENTES, SEM SUCESSO
    while True:
        print("💬 Whisper started")

        # Pega o áudio cru da fila
        audio = await queue.get()

        # Isso aqui é um pé no saco: não importa o tamanho real do áudio,
        # o whisper sempre espera cerca de 30s.
        #
        # Se for menor, ele preenche o resto com zeros (silêncio).
        # Se for maior, ele corta o que passa dos 30s.
        #
        # IMPORTANTE: não tem como passar áudios de durações diferentes
        # usando o código da OpenAI, porque o modelo foi treinado assim.
        audio = whisper.pad_or_trim(audio)

        # O whisper não "ouve" o som, ele transforma em imagem.
        # É um gráfico do som chamado espectrograma (Mel logarítmico).
        # Já expliquei isso num vídeo, é bem de boa de entender.
        mel = whisper.log_mel_spectrogram(audio, n_mels=MODEL.dims.n_mels).to(
            MODEL.device
        )

        options = whisper.DecodingOptions(
            # Último teste: inglês (en)
            # Mas 90% dos testes foram em português (pt), com áudios limpos,
            # de vídeos do YouTube com gente falando claro, sem gíria nem ruído.
            language="en",

            # Tentei deixar mais rápido usando o modo "greedy"
            # Também expliquei isso no outro vídeo
            temperature=0,
            beam_size=1,
            patience=0,  # isso aqui nem faz nada, foi mais no desespero

            # Desativei os timestamps pra ver se o modelo ficava mais leve,
            # focando só na transcrição mesmo
            without_timestamps=True,

            # Aquele prefixo lá de cima... tentei usar nessas duas opções aqui
            # (prompt e prefix). O modelo entrou em loop e fiquei bolado.
            # Desisti de usar.
            prompt=None,
            prefix=None,
        )

        # E aqui é onde rola a transcrição de fato
        result = whisper.decode(MODEL, mel, options)

        prefix = result.text
        print("💬", result.text, "\n")
        queue.task_done()

Tentei detalhar bastante nos comentários do código acima, mas ainda tem umas coisas importantes que vale comentar.

Primeiro: não consegui usar nenhum modelo acima do medium. O motivo é simples, o tempo de transcrição ficava maior do que a janela de tempo que eu escolhi.

Tipo assim: com o modelo turbo e uma janela de 2 segundos, o whisper levava uns 10 segundos (ou mais!) pra cuspir a transcrição. Ou seja, não dava tempo. Não tinha como rodar em tempo real desse jeito. Pode ser limitação do meu hardware também? Pode. Mas o fato é que não rolou.

Os modelos que se saíram melhor nesse esquema foram o tiny e o base. Ambos ainda são imprecisos, mas se eu tivesse que escolher um, ficaria com o base. O tiny, cara… entra em loop com uma facilidade absurda, qualquer coisa repete, embanana, e vira um samba do sussurrador doido.

Sobre os tempos, testei com várias janelas diferentes:

  • ⚠️ 1s, 2s, 3s, 4s: sem chance. O modelo não entende nada, e em poucos ciclos já começa a repetir as coisas.
  • ⚠️ 5s a 9s: até vai, mas ainda rola loop e perda de qualidade em alguns momentos.
  • 10s a 30s: aqui a coisa começou a funcionar de verdade. Foi a faixa que deu o melhor resultado.

Percebeu o paradoxo? Eu queria uma parada em tempo real, mas só começou a ficar usável a partir dos 10 segundos de áudio por vez. Foi aí que me bateu a real: talvez só role se eu refinar o modelo e usar um código customizado, sem depender da implementação padrão do whisper.

Todos esses testes foram feitos usando exatamente o código oficial da OpenAI. Nada modificado no core do modelo.


Conclusão geral

Desse jeito que mostrei até aqui, pra mim não rola usar o whisper com precisão em tempo real. Pelo menos foi essa a conclusão que cheguei depois de tudo isso.

Mas ó: foi uma experiência massa. Aprendi muita coisa que eu nem fazia ideia, principalmente sobre áudio, e me diverti bastante fuçando esse modelo.

Tô deixando tudo documentado aqui porque pode ser que te ajude a ir mais fundo no modelo, ou até mexer no código do whisper pra tentar um resultado melhor que o meu.

Outra opção que eu não testei, mas pode valer muito a pena, seria usar o whisper.cpp (versão C++) ou o faster-whisper, que é uma implementação otimizada em Python com base no CTranslate2.

Valeu demais se tu leu até aqui! Tomara que eu tenha te ajudado ou feito você perder menos tempo se fosse testar tudo isso que testei.

Obs: escrevi esse texto com markdown, se quiser baixar:

Bye 👋!!!


Comentários

No seu e-mail

Newsletter

Junte-se a centenas de outros desenvolvedores e receba dicas e conteúdo técnico diretamente na sua caixa de entrada. Sem SPAM ou publicidade. Apenas conteúdo de qualidade.