Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ The parent `../AGENTS.md` applies.
- Treat stable capture from the glasses microphone over Android Bluetooth communication routing as a hard go/no-go prerequisite.
- Continuous listening must run in a microphone foreground service with visible Android notification and permission handling.
- Keep wake-word detection, voice activity detection, speech understanding, action validation, note storage, and TTS local to the phone.
- Use the bundled Apache-2.0 Vosk Android runtime and Brazilian Portuguese small model only for grammar-constrained wake and service commands. Transcribe bounded note audio in batch with the vendored MIT-licensed `whisper.cpp` v1.9.1 runtime and multilingual Whisper `small-q5_1`, fixed to Portuguese (`pt`). Build native code only for `arm64-v8a` initially. Do not add network recognition as a fallback.
- Download the official Whisper model once on first activation, validate its pinned SHA-1, and store it in private app storage. Network access is allowed only for this explicit model acquisition; inference remains local. Removing app data may require downloading the model again.
- Use the Apache-2.0 Vosk Android runtime only for grammar-constrained wake, note-ending, cancellation, and service commands. Portuguese (`pt-BR`) uses the bundled `vosk-model-small-pt-0.3`; English (`en-US`) and Spanish (`es-ES`) download their official small Vosk models on first selection. Load only the selected language model. Transcribe bounded note audio in batch with the vendored MIT-licensed `whisper.cpp` v1.9.1 runtime and multilingual Whisper `small-q5_1`, passing the selected language (`pt`, `en`, or `es`) explicitly. Build native code only for `arm64-v8a` initially. Do not add network recognition as a fallback.
- Download official Whisper and optional Vosk language models only when required, validate pinned hashes, and store extracted models in private app storage. Network access is allowed only for this explicit model acquisition; recognition and transcription remain local. Removing app data may require downloading the models again.
- Load Whisper only for bounded batch transcription, retain it for up to five minutes after its latest use to accelerate consecutive notes, then release it while waiting. Always release it when the service stops. Route acknowledgement tones explicitly to the selected Bluetooth communication device.
- Initial wake phrases are `nova anotação` and `anote para mim`. Accept the conservative recognition variants `nova notação`, `nova nota`, `ova anotação`, `ova notação`, and `ova nota`, but never accept `anotação` alone as a wake phrase. While dictating, save after four seconds of silence or when `fim da anotação`, `fim da nota`, or `encerrar anotação` is recognized; omit the ending command from the saved note. Recognition text may exist transiently in memory, but continuous audio and intermediate transcripts must not be persisted.
- Keep language definitions centralized in `SpeechLanguage`; do not mix several wake languages in one active recognizer. Portuguese remains the default and preserves its conservative variants without accepting `anotação` alone. English and Spanish are the first optional languages and must remain labeled experimental until physically validated by fluent speakers. Add German, Italian, Chinese, Japanese, or Korean only with a compatible local model, natural phrases reviewed by a fluent speaker, pinned download integrity, and tests. While dictating, save after four seconds of silence or a configured language-specific ending phrase; omit the ending command from the saved note. Recognition text may exist transiently in memory, but continuous audio and intermediate transcripts must not be persisted.
- Keep continuous audio in a bounded in-memory buffer. Do not persist audio except during an explicit diagnostic recording or when the user explicitly chooses an audio note.
- Validate AI-produced actions through deterministic application rules before storing or executing them.
- Store notes locally. External export or synchronization must be optional and require a separate product decision.
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Core transcription does not use ChatGPT, a backend, or a hosted speech service.

### Highlights

- Brazilian Portuguese voice commands and transcription;
- selectable Brazilian Portuguese voice commands plus experimental English and Spanish support;
- on-device wake phrase detection with Vosk;
- local batch transcription with multilingual Whisper `small-q5_1`;
- private local note storage, selection, deletion, and Android sharing;
Expand All @@ -26,10 +26,11 @@ A arquitetura pode ser adaptada para outras marcas e modelos que disponibilizem
## Funcionamento

1. Ative o modo de anotações no aplicativo.
2. Diga **“Nova anotação”** ou **“Anote para mim”**.
3. Aguarde o sinal sonoro enviado pela rota Bluetooth e dite a nota.
4. Diga **“Fim da anotação”**, **“Fim da nota”** ou **“Encerrar anotação”**, ou permaneça em silêncio por quatro segundos.
5. O texto é salvo no armazenamento privado do aplicativo e o áudio transitório é descartado.
2. Selecione o idioma da fala. Português é o padrão validado; inglês e espanhol são opcionais e ainda precisam de validação física por falantes desses idiomas.
3. Diga **“Nova anotação”** ou **“Anote para mim”** em português, **“New note”** em inglês ou **“Nueva nota”** em espanhol.
4. Aguarde o sinal sonoro enviado pela rota Bluetooth e dite a nota.
5. Use um comando de encerramento do idioma selecionado ou permaneça em silêncio por quatro segundos.
6. O texto é salvo no armazenamento privado do aplicativo e o áudio transitório é descartado.

As anotações podem ser selecionadas individualmente ou todas de uma vez. A seleção pode ser excluída após confirmação ou compartilhada como um único texto, com uma linha em branco entre as notas, pelo seletor padrão do Android (incluindo Samsung Notes, WhatsApp e ChatGPT quando disponíveis).

Expand All @@ -40,12 +41,12 @@ O pipeline é híbrido para equilibrar precisão, latência e bateria:
- Vosk com vocabulário restrito permanece ativo para detectar os comandos;
- Whisper `small-q5_1`, executado pelo `whisper.cpp`, transcreve somente a fala delimitada;
- o Whisper é carregado sob demanda, reutilizado por até cinco minutos entre notas e depois liberado para reduzir a memória em espera;
- o idioma do Whisper é fixado em português (`pt`);
- o idioma selecionado é passado explicitamente ao Whisper (`pt`, `en` ou `es`);
- a inferência do Whisper ocorre em lote e fora da thread principal;
- o código nativo usa otimização `-O3` e até seis núcleos para reduzir a latência das notas;
- nenhum áudio ou texto é enviado para transcrição na nuvem.

Na primeira ativação, o aplicativo precisa de internet para baixar aproximadamente 190 MB do modelo oficial `ggml-small-q5_1.bin`. O arquivo é validado por SHA-1 e armazenado no diretório privado do aplicativo. Após isso, o fluxo funciona offline. Limpar os dados ou desinstalar o aplicativo remove o modelo e exige um novo download.
Na primeira ativação, o aplicativo precisa de internet para baixar aproximadamente 190 MB do modelo oficial `ggml-small-q5_1.bin`. Português usa o modelo Vosk incluído no APK. Na primeira seleção de inglês ou espanhol, o respectivo modelo Vosk oficial, com cerca de 40 MB, também é baixado. Todos os downloads são validados por hash e os modelos ficam no diretório privado do aplicativo. Após isso, o idioma instalado funciona offline. Limpar os dados ou desinstalar o aplicativo remove os modelos e exige novos downloads.

## Diagnóstico

Expand All @@ -55,7 +56,7 @@ O botão **Gravar teste** cria deliberadamente um WAV no armazenamento privado p

- Kotlin e Jetpack Compose/Material 3;
- JVM 17, `minSdk 31`, `compileSdk 36` e `targetSdk 36`;
- Vosk Android com `vosk-model-small-pt-0.3`;
- Vosk Android com `vosk-model-small-pt-0.3`, `vosk-model-small-en-us-0.15` ou `vosk-model-small-es-0.42`;
- `whisper.cpp` v1.9.1 compilado para `arm64-v8a`;
- Whisper multilíngue `small-q5_1`;
- NDK `25.2.9519653` e CMake `3.22.1`.
Expand Down
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Vosk

The application uses `com.alphacephei:vosk-android` and the `vosk-model-small-pt-0.3` Portuguese speech-recognition model. Both are distributed by Alpha Cephei under the Apache License 2.0.
The application uses `com.alphacephei:vosk-android` and the official `vosk-model-small-pt-0.3`, `vosk-model-small-en-us-0.15`, and `vosk-model-small-es-0.42` speech-recognition models. They are distributed by Alpha Cephei under the Apache License 2.0. Portuguese is bundled; English and Spanish are downloaded on first selection and stored privately on the device.

- Runtime: https://github.com/alphacep/vosk-api
- Model: https://alphacephei.com/vosk/models
Expand Down
33 changes: 32 additions & 1 deletion app/src/main/java/com/edson/metaglassesnotes/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import com.edson.metaglassesnotes.audio.AudioMode
import com.edson.metaglassesnotes.audio.GlassesAudioService
import com.edson.metaglassesnotes.notes.LocalNote
import com.edson.metaglassesnotes.notes.LocalNoteRepository
import com.edson.metaglassesnotes.speech.SpeechLanguage
import com.edson.metaglassesnotes.speech.SpeechLanguagePreferences
import java.text.DateFormat
import java.util.Date

Expand All @@ -60,6 +62,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AudioCaptureController.update { it.copy(notes = LocalNoteRepository(this).list()) }
var selectedLanguage by mutableStateOf(SpeechLanguagePreferences.get(this))
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Expand All @@ -72,6 +75,11 @@ class MainActivity : ComponentActivity() {
play = ::playLastRecording,
deleteNotes = ::deleteNotes,
shareNotes = ::shareNotes,
selectedLanguage = selectedLanguage,
selectLanguage = { language ->
SpeechLanguagePreferences.set(this, language)
selectedLanguage = language
},
)
}
}
Expand Down Expand Up @@ -144,6 +152,8 @@ private fun NotesScreen(
play: () -> Unit,
deleteNotes: (List<LocalNote>) -> Unit,
shareNotes: (List<LocalNote>) -> Unit,
selectedLanguage: SpeechLanguage,
selectLanguage: (SpeechLanguage) -> Unit,
) {
val state by AudioCaptureController.state.collectAsStateWithLifecycle()
var selectedIds by remember { mutableStateOf(emptySet<Long>()) }
Expand Down Expand Up @@ -183,6 +193,27 @@ private fun NotesScreen(
Text(stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium)
Text("Anotações privadas pelos seus óculos", style = MaterialTheme.typography.titleMedium)
}
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Idioma da fala", style = MaterialTheme.typography.titleMedium)
Text(
"O idioma selecionado define o modelo do gatilho e da transcrição.",
style = MaterialTheme.typography.bodySmall,
)
SpeechLanguage.entries.forEach { language ->
OutlinedButton(
onClick = { selectLanguage(language) },
enabled = state.mode == AudioMode.IDLE,
modifier = Modifier.fillMaxWidth(),
) {
val marker = if (language == selectedLanguage) "✓ " else ""
Text("$marker${language.displayName}")
}
}
}
}
}
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Expand All @@ -208,7 +239,7 @@ private fun NotesScreen(
}
item {
Text(
"Com o modo ativo, diga “Nova anotação”, aguarde o sinal e fale. O áudio é processado localmente e descartado.",
"Com o modo ativo, diga “${selectedLanguage.primaryWakePhrase}”, aguarde o sinal e fale. O áudio é processado localmente e descartado.",
style = MaterialTheme.typography.bodySmall,
)
}
Expand Down
Loading