cesarlopes/digitaia-dotnet

Ditado por voz local para Windows, com transcrição via Whisper e revisão inteligente via Qwen — feito em C#/.NET e WPF.

C#

0

2 commits

updated Sep 10, 2026

See the code

README

DigitaIA

Ditado por voz local para Windows. Você segura uma tecla, fala, e o texto aparece no campo onde já estava digitando.

Tudo roda na sua máquina. Sem API externa, sem nuvem, sem Ollama, sem Python, sem telemetria. O áudio nunca sai do processo e nunca toca o disco.

Ctrl + Espaço (segurar)
        │
        ├── captura a janela ativa
        ├── grava pelo WASAPI, mono 16 kHz, em memória
        │
   (soltar)
        │
        ├── whisper.cpp  ──►  transcrição
        │
        ├── Modo Literal      ──►  texto direto
        └── Modo Inteligente  ──►  llama.cpp + Qwen  ──►  texto revisado
                │
                ├── devolve o foco à janela original
                └── SendInput ou clipboard
                        │
                        ▼
        Outlook · Chrome · Edge · WhatsApp · PhpStorm
        VS Code · Bloco de Notas · Word · Slack · Teams

Estado atual

Funciona de ponta a ponta e foi medido nesta máquina, numa RTX 3070 com áudio de nove segundos em português:

EtapaBackendTempo
Transcrição (Whisper large-v3-turbo q5_0)Vulkan361 ms
Revisão (Qwen3-4B-Instruct Q4_K_M)Vulkan472 ms
Inserção por SendInput, 170 caracteresWin32155 ms
Inserção por clipboard, 1967 caracteresWin32965 ms

Entrada e saída do teste de aceitação:

falado    : "Olá Lucas bom dia verificamos aqui e aparentemente
             o email já está funcionando poderia fazer um novo teste"

literal   : "Olá Lucas, bom dia, verificamos aqui e aparentemente
             o e-mail já está funcionando, poderia fazer um novo teste."

revisado  : "Olá Lucas, bom dia. Verificamos aqui e aparentemente
             o e-mail já está funcionando. Poderia fazer um novo teste?"

Requisitos

  • Windows 10 ou 11, x64
  • .NET 10 SDK para compilar; o runtime basta para executar
  • Microfone
  • GPU com driver Vulkan para aceleração. Sem ela o app roda em CPU

Placa NVIDIA, AMD ou Intel servem: o backend usa Vulkan, que acompanha o driver de vídeo. Não é preciso instalar CUDA Toolkit. Veja a seção sobre GPU mais abaixo.


Compilar e executar

dotnet build
dotnet test
dotnet run --project src/DigitaIA.App

Para gerar a versão distribuível:

dotnet publish src/DigitaIA.App -c Release -r win-x64 --self-contained true

A saída fica com cerca de 375 MB, quase toda em bibliotecas nativas de inferência. Os modelos ficam de fora, em %LOCALAPPDATA%.

O projeto traz um NuGet.Config próprio apontando para o nuget.org, porque a configuração global desta máquina estava sem fontes.


Modelos

Nada é embutido no executável. Coloque os arquivos em:

%LOCALAPPDATA%\DigitaIA\Models\
├── Whisper\ggml-large-v3-turbo-q5_0.bin        574 MB
└── LLM\Qwen3-4B-Instruct-2507-Q4_K_M.gguf      2,4 GB

O app cria essas pastas na primeira execução e avisa quando um modelo falta, sem quebrar. O Modo Literal funciona só com o Whisper; o modelo de linguagem é necessário apenas para o Modo Inteligente.

Whisper

curl.exe -L -o "$env:LOCALAPPDATA\DigitaIA\Models\Whisper\ggml-large-v3-turbo-q5_0.bin" `
  https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin

O large-v3-turbo quantizado em q5_0 é o melhor equilíbrio para português brasileiro numa placa de 8 GB. Qualquer ggml-*.bin na pasta serve; se o arquivo recomendado não existir, o app usa o primeiro que encontrar.

Qwen

curl.exe -L -o "$env:LOCALAPPDATA\DigitaIA\Models\LLM\Qwen3-4B-Instruct-2507-Q4_K_M.gguf" `
  https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_M.gguf

A variante Instruct-2507 não emite blocos de raciocínio, então a saída já vem limpa. Qualquer .gguf na pasta serve.


GPU

O app usa Vulkan para acelerar Whisper e Qwen. Vulkan precisa apenas do vulkan-1.dll, que vem junto com o driver de vídeo, então funciona sem instalar nada.

CUDA está antes do Vulkan na ordem de carregamento: se a máquina tiver o CUDA Toolkit instalado, ele é usado; se não, a tentativa falha e o app segue no Vulkan, depois em CPU. Nunca quebra por falta de GPU.

Os pacotes CUDA do whisper.cpp e do llama.cpp não embutem o runtime CUDA, e por isso não foram distribuídos aqui. O raciocínio completo, com a tabela de dependências que mostra isso, está em docs/DECISIONS.md.

O backend em uso aparece na janela principal, em "Processamento", e no log.

VRAM ocupada com os dois modelos carregados: cerca de 3,8 GB.


Configuração

%LOCALAPPDATA%\DigitaIA\settings.json, criado com padrões na primeira execução:

{
  "mode": "Literal",
  "hotkey": "Ctrl+Space",
  "language": "pt",
  "whisperModel": "default",
  "llmModel": "default",
  "gpuEnabled": true,
  "llmGpuLayers": -1,
  "llmContextSize": 4096,
  "audioDeviceId": "",
  "clipboardInjectionThreshold": 600,
  "restoreClipboard": true,
  "minimumRecordingMilliseconds": 300,
  "logTranscripts": false,
  "showOverlay": true,
  "blockHotkeyPassthrough": true
}

Campos que merecem explicação:

  • hotkey aceita combinações como Ctrl+Space, Ctrl+Alt+Space, F9, ou um modificador solto como RightCtrl.
  • blockHotkeyPassthrough consome a combinação para o autocompletar do PhpStorm e do VS Code não disparar enquanto você fala. Desligue se preferir manter o atalho original desses editores.
  • language usa código curto (pt, en, es) ou auto para detecção automática.
  • clipboardInjectionThreshold é o tamanho a partir do qual a inserção passa a usar clipboard em vez de digitar caractere a caractere.
  • logTranscripts liga o registro do texto ditado no log. Desligado por padrão, por privacidade.
  • llmGpuLayers com -1 tenta colocar tudo na GPU. Reduza se a VRAM apertar.

Um arquivo corrompido ou incompleto não derruba o app: os campos ausentes voltam ao padrão.


Estrutura

DigitaIA.sln
Directory.Build.props        TFM, versões de pacote, propriedades comuns
Directory.Build.targets      implicit usings que o SDK do WPF descarta

src/
├── DigitaIA.Core            contratos, estados, configuração, pipeline
│                            net10.0 puro, sem nada de Windows
├── DigitaIA.Windows         hook de teclado, SendInput, clipboard, janela ativa
├── DigitaIA.Audio           captura WASAPI e conversão para o formato do Whisper
├── DigitaIA.Transcription   Whisper.net
├── DigitaIA.AI              LLamaSharp e prompts
└── DigitaIA.App             WPF, bandeja, overlay, injeção de dependência

tests/
├── DigitaIA.Core.Tests      pipeline, estados, configuração, prompts  (80)
└── DigitaIA.Audio.Tests     conversão e reamostragem de áudio          (15)

docs/DECISIONS.md            decisões de arquitetura e o que foi medido

DigitaIA.Core não referencia nenhum outro projeto. É isso que permite testar o pipeline inteiro com mocks, sem tocar em Win32 nem carregar modelos.

Peças principais

TipoPapel
DictationPipelineorquestra o ciclo, sem saber que existe UI
StateMachinerecusa transições inválidas e gravações concorrentes
LowLevelHotkeyServiceWH_KEYBOARD_LL com key-down e key-up
WasapiAudioCaptureServicecaptura em memória e aquecimento do dispositivo
WhisperTranscriptionServicemodelo residente, backend reportado
LlamaTextEnhancementServiceStatelessExecutor, revisão de turno único
CompositeTextInjectionStrategyescolhe SendInput ou clipboard pelo tamanho
ActiveWindowServicefotografa e restaura a janela do usuário

Interface

O app vive na bandeja. Clicar no ícone abre uma janela pequena com status, modo, atalho, modelo e backend. Fechar essa janela apenas esconde; sair de vez é pelo menu de contexto do ícone.

O ícone muda de cor conforme o estado: azul parado, vermelho gravando, laranja processando.

Durante o ditado aparece um overlay discreto no canto inferior direito, com 🎙 Fala aí..., Transcrevendo..., Revisando... e ✓ Pronto. Ele nunca recebe foco e some sozinho.


Logs

%LOCALAPPDATA%\DigitaIA\Logs\digitaia-AAAAMMDD.log, com rotação diária e 14 dias de retenção.

Registra início e fim de gravação, duração do áudio, tempo de transcrição, tempo do LLM, tempo de inserção, processo da janela ativa, backend e erros.

Não registra áudio, nunca. Não registra o texto ditado, a menos que logTranscripts seja ligado.


Limitações atuais

  • A janela de configurações ainda não existe. O botão explica onde fica o settings.json. Trocar de modo funciona pela bandeja e pela janela principal.
  • Sem downloader de modelos. A abstração IModelManager já prevê download com progresso e checksum, mas DownloadAsync lança NotSupportedException. Baixe os arquivos manualmente.
  • A hotkey não funciona sobre janelas elevadas. Um hook em processo sem elevação não recebe teclas enquanto o Gerenciador de Tarefas, ou outra janela como administrador, está em primeiro plano. É o modelo de segurança do Windows.
  • Sem seleção de microfone na interface. A camada de áudio já enumera dispositivos e o settings.json aceita audioDeviceId, mas não há tela.
  • Sem context awareness. A janela ativa é capturada e usada só para restaurar o foco e para o log. Nada do conteúdo dela vai para o LLM.
  • Sem instalador e sem iniciar com o Windows.
  • Uma gravação por vez. Segurar a hotkey durante um processamento em andamento é recusado com aviso, em vez de enfileirar.

Próximos passos

  1. Janela de configurações com atalho, idioma, microfone, modo e caminhos.
  2. Downloader de modelos com progresso, checksum e validação.
  3. Seleção de dispositivo de áudio e indicador de nível durante a gravação.
  4. Perfis de prompt além da revisão: tom formal, tradução, resumo.
  5. Context awareness usando o processo e o título da janela ativa para ajustar o prompt.
  6. Instalador e opção de iniciar com o Windows.
  7. Cancelar o ditado em andamento com Esc.

Privacidade

Não existe telemetria, analytics, Sentry, nem chamada a serviço de IA remoto. Não há código de rede no projeto fora do downloader ainda não implementado.

Áudio, transcrição e texto revisado ficam na memória do processo e vão apenas para a janela que você escolheu.

cesarlopes/digitaia-dotnet

Ditado por voz local para Windows, com transcrição via Whisper e revisão inteligente via Qwen — feito em C#/.NET e WPF.

C#

0

2 commits

updated Sep 10, 2026

See the code

README

DigitaIA

Ditado por voz local para Windows. Você segura uma tecla, fala, e o texto aparece no campo onde já estava digitando.

Tudo roda na sua máquina. Sem API externa, sem nuvem, sem Ollama, sem Python, sem telemetria. O áudio nunca sai do processo e nunca toca o disco.

Ctrl + Espaço (segurar)
        │
        ├── captura a janela ativa
        ├── grava pelo WASAPI, mono 16 kHz, em memória
        │
   (soltar)
        │
        ├── whisper.cpp  ──►  transcrição
        │
        ├── Modo Literal      ──►  texto direto
        └── Modo Inteligente  ──►  llama.cpp + Qwen  ──►  texto revisado
                │
                ├── devolve o foco à janela original
                └── SendInput ou clipboard
                        │
                        ▼
        Outlook · Chrome · Edge · WhatsApp · PhpStorm
        VS Code · Bloco de Notas · Word · Slack · Teams

Estado atual

Funciona de ponta a ponta e foi medido nesta máquina, numa RTX 3070 com áudio de nove segundos em português:

EtapaBackendTempo
Transcrição (Whisper large-v3-turbo q5_0)Vulkan361 ms
Revisão (Qwen3-4B-Instruct Q4_K_M)Vulkan472 ms
Inserção por SendInput, 170 caracteresWin32155 ms
Inserção por clipboard, 1967 caracteresWin32965 ms

Entrada e saída do teste de aceitação:

falado    : "Olá Lucas bom dia verificamos aqui e aparentemente
             o email já está funcionando poderia fazer um novo teste"

literal   : "Olá Lucas, bom dia, verificamos aqui e aparentemente
             o e-mail já está funcionando, poderia fazer um novo teste."

revisado  : "Olá Lucas, bom dia. Verificamos aqui e aparentemente
             o e-mail já está funcionando. Poderia fazer um novo teste?"

Requisitos

  • Windows 10 ou 11, x64
  • .NET 10 SDK para compilar; o runtime basta para executar
  • Microfone
  • GPU com driver Vulkan para aceleração. Sem ela o app roda em CPU

Placa NVIDIA, AMD ou Intel servem: o backend usa Vulkan, que acompanha o driver de vídeo. Não é preciso instalar CUDA Toolkit. Veja a seção sobre GPU mais abaixo.


Compilar e executar

dotnet build
dotnet test
dotnet run --project src/DigitaIA.App

Para gerar a versão distribuível:

dotnet publish src/DigitaIA.App -c Release -r win-x64 --self-contained true

A saída fica com cerca de 375 MB, quase toda em bibliotecas nativas de inferência. Os modelos ficam de fora, em %LOCALAPPDATA%.

O projeto traz um NuGet.Config próprio apontando para o nuget.org, porque a configuração global desta máquina estava sem fontes.


Modelos

Nada é embutido no executável. Coloque os arquivos em:

%LOCALAPPDATA%\DigitaIA\Models\
├── Whisper\ggml-large-v3-turbo-q5_0.bin        574 MB
└── LLM\Qwen3-4B-Instruct-2507-Q4_K_M.gguf      2,4 GB

O app cria essas pastas na primeira execução e avisa quando um modelo falta, sem quebrar. O Modo Literal funciona só com o Whisper; o modelo de linguagem é necessário apenas para o Modo Inteligente.

Whisper

curl.exe -L -o "$env:LOCALAPPDATA\DigitaIA\Models\Whisper\ggml-large-v3-turbo-q5_0.bin" `
  https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin

O large-v3-turbo quantizado em q5_0 é o melhor equilíbrio para português brasileiro numa placa de 8 GB. Qualquer ggml-*.bin na pasta serve; se o arquivo recomendado não existir, o app usa o primeiro que encontrar.

Qwen

curl.exe -L -o "$env:LOCALAPPDATA\DigitaIA\Models\LLM\Qwen3-4B-Instruct-2507-Q4_K_M.gguf" `
  https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_M.gguf

A variante Instruct-2507 não emite blocos de raciocínio, então a saída já vem limpa. Qualquer .gguf na pasta serve.


GPU

O app usa Vulkan para acelerar Whisper e Qwen. Vulkan precisa apenas do vulkan-1.dll, que vem junto com o driver de vídeo, então funciona sem instalar nada.

CUDA está antes do Vulkan na ordem de carregamento: se a máquina tiver o CUDA Toolkit instalado, ele é usado; se não, a tentativa falha e o app segue no Vulkan, depois em CPU. Nunca quebra por falta de GPU.

Os pacotes CUDA do whisper.cpp e do llama.cpp não embutem o runtime CUDA, e por isso não foram distribuídos aqui. O raciocínio completo, com a tabela de dependências que mostra isso, está em docs/DECISIONS.md.

O backend em uso aparece na janela principal, em "Processamento", e no log.

VRAM ocupada com os dois modelos carregados: cerca de 3,8 GB.


Configuração

%LOCALAPPDATA%\DigitaIA\settings.json, criado com padrões na primeira execução:

{
  "mode": "Literal",
  "hotkey": "Ctrl+Space",
  "language": "pt",
  "whisperModel": "default",
  "llmModel": "default",
  "gpuEnabled": true,
  "llmGpuLayers": -1,
  "llmContextSize": 4096,
  "audioDeviceId": "",
  "clipboardInjectionThreshold": 600,
  "restoreClipboard": true,
  "minimumRecordingMilliseconds": 300,
  "logTranscripts": false,
  "showOverlay": true,
  "blockHotkeyPassthrough": true
}

Campos que merecem explicação:

  • hotkey aceita combinações como Ctrl+Space, Ctrl+Alt+Space, F9, ou um modificador solto como RightCtrl.
  • blockHotkeyPassthrough consome a combinação para o autocompletar do PhpStorm e do VS Code não disparar enquanto você fala. Desligue se preferir manter o atalho original desses editores.
  • language usa código curto (pt, en, es) ou auto para detecção automática.
  • clipboardInjectionThreshold é o tamanho a partir do qual a inserção passa a usar clipboard em vez de digitar caractere a caractere.
  • logTranscripts liga o registro do texto ditado no log. Desligado por padrão, por privacidade.
  • llmGpuLayers com -1 tenta colocar tudo na GPU. Reduza se a VRAM apertar.

Um arquivo corrompido ou incompleto não derruba o app: os campos ausentes voltam ao padrão.


Estrutura

DigitaIA.sln
Directory.Build.props        TFM, versões de pacote, propriedades comuns
Directory.Build.targets      implicit usings que o SDK do WPF descarta

src/
├── DigitaIA.Core            contratos, estados, configuração, pipeline
│                            net10.0 puro, sem nada de Windows
├── DigitaIA.Windows         hook de teclado, SendInput, clipboard, janela ativa
├── DigitaIA.Audio           captura WASAPI e conversão para o formato do Whisper
├── DigitaIA.Transcription   Whisper.net
├── DigitaIA.AI              LLamaSharp e prompts
└── DigitaIA.App             WPF, bandeja, overlay, injeção de dependência

tests/
├── DigitaIA.Core.Tests      pipeline, estados, configuração, prompts  (80)
└── DigitaIA.Audio.Tests     conversão e reamostragem de áudio          (15)

docs/DECISIONS.md            decisões de arquitetura e o que foi medido

DigitaIA.Core não referencia nenhum outro projeto. É isso que permite testar o pipeline inteiro com mocks, sem tocar em Win32 nem carregar modelos.

Peças principais

TipoPapel
DictationPipelineorquestra o ciclo, sem saber que existe UI
StateMachinerecusa transições inválidas e gravações concorrentes
LowLevelHotkeyServiceWH_KEYBOARD_LL com key-down e key-up
WasapiAudioCaptureServicecaptura em memória e aquecimento do dispositivo
WhisperTranscriptionServicemodelo residente, backend reportado
LlamaTextEnhancementServiceStatelessExecutor, revisão de turno único
CompositeTextInjectionStrategyescolhe SendInput ou clipboard pelo tamanho
ActiveWindowServicefotografa e restaura a janela do usuário

Interface

O app vive na bandeja. Clicar no ícone abre uma janela pequena com status, modo, atalho, modelo e backend. Fechar essa janela apenas esconde; sair de vez é pelo menu de contexto do ícone.

O ícone muda de cor conforme o estado: azul parado, vermelho gravando, laranja processando.

Durante o ditado aparece um overlay discreto no canto inferior direito, com 🎙 Fala aí..., Transcrevendo..., Revisando... e ✓ Pronto. Ele nunca recebe foco e some sozinho.


Logs

%LOCALAPPDATA%\DigitaIA\Logs\digitaia-AAAAMMDD.log, com rotação diária e 14 dias de retenção.

Registra início e fim de gravação, duração do áudio, tempo de transcrição, tempo do LLM, tempo de inserção, processo da janela ativa, backend e erros.

Não registra áudio, nunca. Não registra o texto ditado, a menos que logTranscripts seja ligado.


Limitações atuais

  • A janela de configurações ainda não existe. O botão explica onde fica o settings.json. Trocar de modo funciona pela bandeja e pela janela principal.
  • Sem downloader de modelos. A abstração IModelManager já prevê download com progresso e checksum, mas DownloadAsync lança NotSupportedException. Baixe os arquivos manualmente.
  • A hotkey não funciona sobre janelas elevadas. Um hook em processo sem elevação não recebe teclas enquanto o Gerenciador de Tarefas, ou outra janela como administrador, está em primeiro plano. É o modelo de segurança do Windows.
  • Sem seleção de microfone na interface. A camada de áudio já enumera dispositivos e o settings.json aceita audioDeviceId, mas não há tela.
  • Sem context awareness. A janela ativa é capturada e usada só para restaurar o foco e para o log. Nada do conteúdo dela vai para o LLM.
  • Sem instalador e sem iniciar com o Windows.
  • Uma gravação por vez. Segurar a hotkey durante um processamento em andamento é recusado com aviso, em vez de enfileirar.

Próximos passos

  1. Janela de configurações com atalho, idioma, microfone, modo e caminhos.
  2. Downloader de modelos com progresso, checksum e validação.
  3. Seleção de dispositivo de áudio e indicador de nível durante a gravação.
  4. Perfis de prompt além da revisão: tom formal, tradução, resumo.
  5. Context awareness usando o processo e o título da janela ativa para ajustar o prompt.
  6. Instalador e opção de iniciar com o Windows.
  7. Cancelar o ditado em andamento com Esc.

Privacidade

Não existe telemetria, analytics, Sentry, nem chamada a serviço de IA remoto. Não há código de rede no projeto fora do downloader ainda não implementado.

Áudio, transcrição e texto revisado ficam na memória do processo e vão apenas para a janela que você escolheu.

Languages

C#

100.0%