- Kotlin 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| app | ||
| art/icon | ||
| docs/screenshots | ||
| gradle | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
![]()
PeggyPad
An Android app for live meeting recording/transcription/summary generation. Inspired by Mad Men's Peggy Olson starting out as a secretary, growing out to copywriter. Install the APK from the releases page, or let Obtainium keep it up to date (see Releases & Obtainium below).
⚠️ AI-generated code
This app was entirely written by AI through conversational prompting with a human collaborator. Every line of code, configuration, and this README was AI-generated.
Screenshots
![]() Your recordings |
![]() One-tap recording |
![]() Minutes by the LLM you choose |
![]() Transcript with speakers |
![]() Your own APIs and vault |
Privacy model
- Network only for what you start. The app makes no network connections at all until you configure an endpoint and tap Transcribe or Summarize on a recording. It shows where the data is going and asks you to confirm. Transcription sends the audio to your speech-to-text endpoint, plus names and terms from the recording's participants and notes as spelling hints; summarizing sends only the transcript, title, date, participants and notes (never the audio) to your summary endpoint. Networking uses the platform HTTP stack, with no third-party SDKs.
- API keys are encrypted with an AES-256-GCM key that stays in the Android Keystore. The key is never shown again after you save it.
- A key only goes where it was saved for. It is bound to its endpoint's origin (scheme, host and port). Saving a different endpoint removes it, and Settings warns before you save, so switching vendor or mistyping a URL never sends your key elsewhere. Transcription requests don't follow redirects, so a key can't be passed on to another host either.
- HTTPS for anything on the internet. Plain
http://is only accepted for this device, private addresses (RFC 191810/8,172.16/12,192.168/16, and IPv6fc00::/7) and private TLDs (.lan,.home,.corp,.internal,.home.arpa,.local), so you can reach a self-hosted server on your own network. Before every plain-HTTP request the app checks that the name resolves only to private addresses. Settings still warns that traffic to such an endpoint isn't encrypted. Only system CAs are trusted. - You choose where audio goes. By default recordings and their transcripts are saved to
Documents/PeggyPad. Android only lets an app without storage permissions put audio and text side by side inDocuments/orDownload/. In Settings you can pick any other folder through the system folder picker. The app requests no storage permission: it writes its own files through MediaStore, or through a grant for the folder you picked. It keeps that grant while recordings still live in the folder, even after you switch to another one. These are shared folders, so apps with access to them can read the recordings. - No backups.
allowBackup="false"and the data-extraction rules exclude the app's own data (titles, notes, settings) from Google cloud backup and device-to-device transfer. - No tracking. No Google Play Services, Firebase, analytics or crash reporting. The Google-encrypted "dependency info" block is left out of the APK.
- Sharing is explicit. The app only sends a recording to another app when the user picks Export / share.
Features: recording
- One-tap recording in a foreground service, so it keeps going with the screen off. Includes pause/resume, a level meter, and notification controls.
- Recordings go to a user-selectable folder (default
PeggyPad), so you can reach them with any file manager or sync tool. - Audio is Opus in an Ogg container, mono, 48 kHz, 32 kbps (≈14 MB per hour). It's tuned for speech, and common STT APIs accept it. Ogg can be streamed, so a recording cut off by a crash or a killed process is still playable. It is recovered automatically on next launch. While a recording is in progress in the default folder, it is hidden from other apps.
- If the app loses access to a folder you picked, the recording says so and offers to restore access by picking that folder again.
- List, play back, rename, add notes, export/share and delete recordings.
Features: transcription
- In Settings → Speech-to-text, choose an API dialect, then set the endpoint, API key, model,
optional language and speaker detection:
- OpenAI-compatible (
POST {endpoint}/audio/transcriptions): OpenAI, Groq and self-hosted servers such as speaches, faster-whisper-server, whisper.cpp or LocalAI.whisper-1and most self-hosted servers give timestamps. For speaker labels, enable Detect speakers and use a diarizing model such asgpt-4o-transcribe-diarize. With Detect speakers off, models that only return text (e.g.gpt-4o-transcribe) are retried automatically without timestamps. - Mistral (
POST {endpoint}/audio/transcriptions, Voxtral): timestamps, plus speakers when enabled. Mistral can't combine timestamps with a fixed language, so the language is always detected automatically. - Deepgram (
POST {endpoint}/listen): timestamps, plus speakers when enabled. - ElevenLabs (
POST {endpoint}/speech-to-text, Scribe): timestamps, plus speakers when enabled.
- OpenAI-compatible (
- Names and terms from a recording's Participants and Notes are sent along as spelling
hints: Mistral's
context_bias, Deepgram'skeyterm, ElevenLabs'keyterms, or an OpenAI-styleprompt(not for diarizing models, which don't accept one). Only short, term-like pieces are used, at most 100. If a service rejects them, e.g. an older model, the recording is transcribed again without hints. - Once configured, recordings get a Transcribe action (detail screen and list menu). Uploads run in a foreground service, so they finish even if you leave the app. They show progress and can be cancelled.
- The transcript is saved as Markdown (
<audio name>.md) with a small header and one paragraph per turn, e.g.**[12:04] Speaker 2:** …. It is saved next to the audio file. - The list shows when each recording was transcribed. When you delete a recording that has a transcript, you choose whether the transcript goes too. If you keep it, only the audio is deleted: the entry stays in the list as Transcript only, with its title, notes and transcript.
- Request size limits are the provider's. For example, OpenAI accepts up to 25 MB, which is about 1¾ hours at PeggyPad's bitrate.
Features: summaries
- In Settings → Summaries, point PeggyPad at any OpenAI-compatible chat-completions endpoint
(
POST {endpoint}/chat/completions): Mistral, OpenAI, Groq, OpenRouter, or a local Ollama, LM Studio or vLLM server. The endpoint is pre-filled from your speech-to-text settings when that is OpenAI or Mistral. - The speech-to-text API key is reused when (and only when) the summary endpoint has the same origin. You can enter a separate key, e.g. one restricted to certain models; it overrides the shared one and is bound to its own origin.
- The instructions (system prompt) are editable. The built-in default is a professional
minute-taker that writes in the conversation's language, separates decisions from actions
and discussion, and marks unclear facts instead of guessing. It lives in
app/src/main/res/raw/default_summary_prompt.md, and is only stored in settings once you change it, so improvements to the default reach you. - Each recording has a Participants field; names given there help the model replace "Speaker 1" with real names.
- Summarize (detail screen or list menu) runs in the background like transcription. The temperature is fixed at 0.2. Transcripts longer than a configurable limit (100,000 characters by default, roughly 25,000 tokens) are summarised in parts by time, then merged into one document, so smaller models with less context also work.
- A recording still called "Recording " takes its title from the summary's heading (the subject after the document type, e.g. "Offerte renovatie kantoor"). Titles you typed yourself are never replaced; the rename dialog offers the summary's title and the original date as one-tap suggestions.
- The result is saved as
<audio name>.summary.mdnext to the transcript. It ends with a visible footnote saying it was generated, with which model and host, and when, so anyone you share it with can see that too. Nothing is hidden in the file. The app renders it, and you can share it.
Features: notes folder (Obsidian) and audio hashes
- Every recording's audio gets a SHA-256 hash when the recording is finished (older recordings: when first transcribed or summarized). It is written into the transcript header, the summary footnote and the meeting note, and shown on the recording's screen with a Verify button that recomputes it, so you can always check that a transcript or summary belongs to this exact audio and that the audio hasn't changed since. A hash shows the file is unchanged; it doesn't prove who recorded it or when.
- Optionally pick a Notes folder in Settings, e.g. a folder in an Obsidian vault synced with
Syncthing. PeggyPad then writes one note per meeting there, named
2026-10-05 Title.mdby default: properties (date, duration, participants,tags: [meeting],source, the audio's file name and SHA-256), the summary, and the transcript in a collapsed callout. The audio itself stays out of the vault. - The note is written when a transcript or summary is saved, and can be exported for older recordings from the recording's screen. PeggyPad updates (and renames, if the title changed) only a note that is exactly as it wrote it; once you or a sync tool changed it, a new note is written next to it, so your edits are never lost. Deleting a recording leaves its note alone.
- The file name is configurable with Obsidian-style placeholders:
{{title}},{{date:FORMAT}}and{{time:FORMAT}}, using the Moment.js tokens Obsidian's templates use (YYYY YY MMMM MMM MM M DD D dddd ddd HH H hh h mm ss A,[literal text]). For example{{date:YYYYMMDD}} {{title}}gives20261005 Offerte renovatie kantoor.md. Settings shows a live example. Characters that aren't allowed in file names, including/, become spaces. - Participants can be written as
[[links]]for vaults with a note per person.
Building
Requirements: the Android SDK (set sdk.dir in local.properties or ANDROID_HOME). You
don't need a JDK: Gradle downloads JDK 21 through the foojay toolchain resolver if needed.
./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk (id eu.flrn.peggy.debug)
./gradlew assembleRelease # signed if a keystore is configured, see below
Releases & Obtainium
Release APKs must always be signed with the same key, or Android will refuse the update.
- Create a keystore once and keep it safe (if you lose it, users have to reinstall):
keytool -genkeypair -v -keystore release.jks -alias peggy -keyalg RSA -keysize 4096 -validity 10000 - For local release builds, create
keystore.propertiesin the repo root (git-ignored):storeFile=release.jks storePassword=... keyAlias=peggy keyPassword=... - For CI, add the secrets listed at the top of
.forgejo/workflows/release.yml.PEGGY_KEYSTORE_B64is the output ofbase64 -w0 release.jks. - To release, bump
versionCodeandversionNameinapp/build.gradle.kts, commit, then tagv<versionName>and push the tag. The workflow builds the APK and attaches it, plus a SHA-256 file, to a Forgejo release. - In Obtainium, add the app with the repository URL (
https://git.flrn.eu/florian/peggypad). Obtainium detects Forgejo/Gitea and tracks its releases.
Roadmap
- Import recordings, transcripts and summaries from a folder, e.g. after reinstalling.
- More summary APIs, such as Anthropic's Messages API.
License
Copyright (C) 2026 Florian Overkamp
PeggyPad is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
PeggyPad is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
SPDX-License-Identifier: GPL-3.0-or-later




