Skip to content

feat(voice): rich markdown formatting, transcript pages, current-page insertion, UX improvements#66

Merged
tstapler merged 5 commits intomainfrom
stelekit-improve-voice-note-feature
May 3, 2026
Merged

feat(voice): rich markdown formatting, transcript pages, current-page insertion, UX improvements#66
tstapler merged 5 commits intomainfrom
stelekit-improve-voice-note-feature

Conversation

@tstapler
Copy link
Copy Markdown
Owner

@tstapler tstapler commented May 3, 2026

Summary

  • Rich LLM formatting: Updated system prompt with few-shot examples teaches Claude to produce #tags, key:: value properties, **bold**/*italic* emphasis, and - TODO action items from voice transcripts
  • Transcript pages: Long notes (≥20 words, configurable) automatically get a dedicated Voice Note YYYY-MM-DD HH:mm:ss page with source:: backlink, formatted content, and optional raw transcript; short notes stay inline
  • Current-page insertion: Voice notes append to whichever page is open in the editor, falling back to today's journal when no page is open
  • UX fixes: Removed the 10-word minimum that rejected short valid notes; extended Android SpeechRecognizer silence timeout from 3s→6s with a 2s minimum so users can pause to think

What changes

New block format (inline, in journal or current page)

- 📝 Voice note (14:35:22) [[Voice Note 2026-05-02 14:35:22]]
  - TODO Call Alice about [[ProjectX]]
  - Review **proposal** before Friday #meeting

New transcript page (Voice Note 2026-05-02 14:35:22)

source:: [[Journal 2026-05-02]]

- TODO Call Alice about [[ProjectX]]
- Review **proposal** before Friday #meeting

#+BEGIN_QUOTE
raw transcript text
#+END_QUOTE

Short notes (< threshold) stay fully inline — no separate page:

- 📝 Voice note (14:35:22)
  - buy milk

Test plan

  • 59/59 voice unit tests pass
  • Short-note inline path test (2-word transcript, no wikilink)
  • Current-page routing tests (open page → append there; null → journal)
  • includeRawTranscript toggle tests on transcript page content
  • Manual: record "buy milk" — should insert inline, no transcript page created
  • Manual: record a 30-word note — should create Voice Note ... page with wikilink in journal
  • Manual: record while a non-journal page is open — block appears on that page
  • Manual: toggle "Include raw transcript" off — transcript page has no #+BEGIN_QUOTE

Future work

  • FR-7: Continuous recording with VAD silence stripping (tracked in project_plans/voice/requirements.md)

🤖 Generated with Claude Code

… insertion, UX improvements

- Rich LLM system prompt with few-shot examples for #tags, key:: value,
  **bold**/*italic*, and - TODO action item detection
- Long notes (>=20 words, configurable) create a dedicated transcript page
  Voice Note YYYY-MM-DD HH:mm:ss with source:: backlink; short notes inline
- Voice notes insert into the currently-open page, falling back to journal
- Removed 10-word minimum word-count gate
- Android SpeechRecognizer silence timeout: 3s->6s complete, 1.5s->3s
  partial, +2s minimum so users can pause to think
- includeRawTranscript toggle controls #+BEGIN_QUOTE on transcript page
- transcriptPageWordThreshold setting configurable from settings UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 3, 2026 06:38
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

JVM Load Benchmark (Desktop)

Synthetic in-memory benchmark measuring load performance for the desktop (JVM) app.
Comparing 8b8fe87 (this PR) vs d75cc83 (baseline)
Graph config: xlarge — 230 pages

Metric This PR Baseline Delta
Phase 1 TTI ↓ 8ms 10ms -2ms (-20%) ✅
Phase 2 background ↓ 3ms 4ms -1ms (-25%) ✅
Phase 3 index ↓ 16ms 18ms -2ms (-11%) ✅
Total ↓ 27ms 31ms -4ms (-13%) ✅
Write p95 (baseline) ↓ 27ms 47ms -20ms (-43%) ✅
Write p95 (under load) ↓ n/a n/a
Jank factor ↓ n/a n/a
↓ lower is better
Flamegraphs (this PR) **Allocation** — object allocation pressure (JDBC/SQLite churn)

Alloc flamegraph not available

CPU — method-level hotspots by on-CPU time

CPU flamegraph not available

Top SQL queries by total time (this PR) | table:operation | calls | p50 | p99 | max | total | |-----------------|-------|-----|-----|-----|-------| | `pages:select` | 2 | 1ms | 1ms | 1ms | 1ms |
Top allocation hotspots (this PR) `65.6%` byte[]_[k] `6.2%` java.lang.String_[k] `2.6%` jdk.internal.org.objectweb.asm.SymbolTable$Entry_[k] `2.6%` java.lang.Object[]_[k] `2.1%` int[]_[k]
Top CPU hotspots (this PR) `99.5%` /usr/lib/x86_64-linux-gnu/libc.so.6 `0%` java/lang/invoke/AbstractValidatingLambdaMetafactory._[0] `0%` java/util/concurrent/atomic/AtomicLongFieldUpdater$CASUpdater.get_[1] `0%` dev/stapler/stelekit/db/DatabaseWriteActor$1.invokeSuspend_[0] `0%` /tmp/sqlite-3.51.3.0-5ac9c2f9-f0c9-4a67-a3b4-487f183c4e85-libsqlitejdbc.so

…silence until explicit stop

Replace the single-shot listen loop with an accumulating restart loop.
When the recognizer stops due to silence (ERROR_NO_MATCH / ERROR_SPEECH_TIMEOUT)
it restarts automatically and appends the new text to the accumulated transcript.
The coroutine only resolves when stopListening() is called explicitly — setting
stopRequested=true causes the next onResults/onError to return the full text
rather than restarting.

If stopListening() is called between cycles (activeRecognizer==null) the next
startCycle() sees stopRequested=true and resolves immediately.

Also add project_plans/voice/ spec artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds richer voice-note formatting and improves how/where voice transcripts are inserted in the graph, including optional transcript pages for longer notes and better capture UX.

Changes:

  • Expands the default LLM system prompt to encourage richer Logseq-friendly formatting (tags, properties, emphasis, TODO/DONE).
  • Adds transcript-page creation for longer notes, with optional raw-transcript quoting and a configurable word threshold.
  • Routes voice-note insertion to the currently open page (fallback to today’s journal) and tweaks Android SpeechRecognizer silence timing.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
kmp/src/commonMain/kotlin/dev/stapler/stelekit/voice/VoiceSettings.kt Persists new voice settings for raw transcript inclusion + transcript-page threshold.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/voice/VoicePipelineFactory.kt Threads new settings into VoicePipelineConfig.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/voice/VoicePipelineConfig.kt Updates prompt and adds config fields for transcript-page behavior.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/voice/VoiceCaptureViewModel.kt Implements current-page insertion and transcript-page creation + block formatting helpers.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/VoiceCaptureSettings.kt Adds UI controls for raw transcript toggle + word-threshold input.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt Wires current open page UUID into the voice capture VM.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/JournalService.kt Adds appendToPage, transcript page creation, and UUID→page name lookup helpers.
kmp/src/businessTest/kotlin/dev/stapler/stelekit/voice/VoiceSettingsTest.kt Adds tests for the new includeRawTranscript setting.
kmp/src/businessTest/kotlin/dev/stapler/stelekit/voice/VoiceNoteBlockFormatTest.kt Updates tests for new inline/transcript-page block formats and raw-transcript toggling.
kmp/src/businessTest/kotlin/dev/stapler/stelekit/voice/VoiceCaptureViewModelTest.kt Updates routing tests and removes the old minimum-word gate expectations.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/voice/AndroidSpeechRecognizerProvider.kt Adjusts silence/timeout extras to allow longer pauses.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +141 to +143
val wordCount = formattedText.split(Regex("\\s+")).count { it.isNotBlank() }
val useTranscriptPage = wordCount >= pipeline.transcriptPageWordThreshold

Comment on lines 128 to 131
@@ -136,27 +131,93 @@ class VoiceCaptureViewModel(
}
Comment on lines 52 to 57
platformSettings.getString(KEY_TRANSCRIPT_PAGE_WORD_THRESHOLD, "20").toIntOrNull() ?: 20

fun setTranscriptPageWordThreshold(threshold: Int) =
platformSettings.putString(KEY_TRANSCRIPT_PAGE_WORD_THRESHOLD, threshold.toString())

companion object {
Comment on lines +164 to +171
OutlinedTextField(
value = transcriptPageWordThreshold,
onValueChange = { transcriptPageWordThreshold = it; saved = false },
label = { Text("Create transcript page after N words") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
Comment on lines +167 to +187
suspend fun appendToPage(pageUuid: String, content: String) {
val page = pageRepository.getPageByUuid(pageUuid).first().getOrNull()
if (page == null) {
appendToToday(content)
return
}
val blocks = blockRepository.getBlocksForPage(page.uuid).first().getOrNull() ?: emptyList()
val nextPosition = (blocks.maxOfOrNull { it.position } ?: -1) + 1
val newBlock = Block(
uuid = UuidGenerator.generateV7(),
pageUuid = page.uuid,
content = content,
position = nextPosition,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
)
if (writeActor != null) {
writeActor.saveBlock(newBlock)
} else {
blockRepository.saveBlock(newBlock)
}
Comment on lines +59 to +75

// --- includeRawTranscript ---

@Test
fun `getIncludeRawTranscript_should_return_true_by_default`() {
val settings = VoiceSettings(MapSettings())
assertTrue(settings.getIncludeRawTranscript(), "Default should be true")
}

@Test
fun `setIncludeRawTranscript_should_persist_value_across_get_calls`() {
val settings = VoiceSettings(MapSettings())
settings.setIncludeRawTranscript(false)
assertFalse(settings.getIncludeRawTranscript(), "Expected persisted false value")
settings.setIncludeRawTranscript(true)
assertTrue(settings.getIncludeRawTranscript(), "Expected persisted true value after re-setting to true")
}
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

Android Load Benchmark

Instrumented benchmark on an API 30 x86_64 emulator — 500-page synthetic graph.

Comparing 8b8fe87 (this PR) vs d75cc83 (baseline)
Device: API 30 x86_64 emulator — 530 pages loaded

Graph Load

Metric This PR Baseline Delta
Phase 1 TTI ↓ 131ms 105ms +26ms (+25%) ⚠️
Phase 3 index ↓ 3062ms 2460ms +602ms (+24%) ⚠️

Interactive Write Latency (during Phase 3)

Metric This PR Baseline Delta
Write p95 (baseline) ↓ 3ms 1ms +2ms (+200%) ⚠️
Write p95 (during phase 3) ↓ 105ms 226ms -121ms (-54%) ✅
Jank factor ↓ 35x 226x -191x (-85%) ✅
Concurrent writes ↑ 14 11 +3ms (+27%) ✅

SAF I/O Overhead (ContentProvider vs direct File read)

Measures Binder IPC cost added by ContentResolver per readFile() call.
Real SAF via ExternalStorageProvider will be higher on device; this is a lower bound.

Metric This PR Baseline Delta
Direct read / file ↓ 0.0ms 0.0ms +0ms ⚠️
Provider read / file ↓ 0.3ms 0.2ms +0ms (+35%) ⚠️
IPC overhead ratio ↓ 8x 6x +2x (+33%) ⚠️
↓ lower is better · ↑ higher is better

tstapler and others added 3 commits May 2, 2026 23:44
…cle()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use rawTranscript (not formattedText) for transcript page word-count
  threshold decision — LLM expansion/compression no longer skews routing
- Replace println with Logger for LLM failure warning
- Clamp transcriptPageWordThreshold to >= 1 in getter, setter, and UI save
- Extract appendBlockToPage() private helper shared by appendToToday/appendToPage
- Add VoiceSettingsTest coverage for threshold default, persistence, and clamping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tstapler tstapler merged commit 8155563 into main May 3, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants