<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Falki</title>
    <description>The latest articles on DEV Community by Falki (@falki_c12885af24ba).</description>
    <link>https://dev.to/falki_c12885af24ba</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982448%2F7c6b11f1-dc0c-4e48-997b-3b0ed6ae44e3.png</url>
      <title>DEV Community: Falki</title>
      <link>https://dev.to/falki_c12885af24ba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/falki_c12885af24ba"/>
    <language>en</language>
    <item>
      <title>Shipping an Android podcast player with on-device Whisper — 6 lessons from a Czech indie launch</title>
      <dc:creator>Falki</dc:creator>
      <pubDate>Sat, 13 Jun 2026 09:45:33 +0000</pubDate>
      <link>https://dev.to/falki_c12885af24ba/shipping-an-android-podcast-player-with-on-device-whisper-6-lessons-from-a-czech-indie-launch-1n93</link>
      <guid>https://dev.to/falki_c12885af24ba/shipping-an-android-podcast-player-with-on-device-whisper-6-lessons-from-a-czech-indie-launch-1n93</guid>
      <description>&lt;p&gt;After a year of nights and weekends, I shipped &lt;strong&gt;Lucidcast&lt;/strong&gt; — an&lt;br&gt;
Android podcast player I built because every mainstream app I tried&lt;br&gt;
was missing the same three things. The app went live on Google Play&lt;br&gt;
this week. This post is for indie devs thinking about shipping&lt;br&gt;
their own Android project — six lessons I learned the expensive way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Lucidcast does (90-second context)
&lt;/h2&gt;

&lt;p&gt;A podcast player with on-device AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whisper transcribes downloaded episodes locally&lt;/strong&gt; (audio never
leaves the phone)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI episode summaries&lt;/strong&gt; via Gemini, with a live progress ring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Podcast" wake-word voice commands&lt;/strong&gt; that work on the lock
screen and in Android Auto&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Pause&lt;/strong&gt; auto-pauses on loud noises or when someone talks
to you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus the usual: chapters, transcripts, value tags (Podcasting 2.0),&lt;br&gt;
22 languages, Android Auto, live radio, no accounts, no ads.&lt;/p&gt;

&lt;p&gt;Free includes the full podcast player. Pro one-time unlocks the&lt;br&gt;
audio intelligence engine. AI Pack subscription enables Whisper +&lt;br&gt;
summaries. Made in EU by a one-person Czech indie team&lt;br&gt;
(Prismatic s.r.o.).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=cz.prismatic.lucidcast" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;&lt;br&gt;
| &lt;a href="https://www.prismatic.cz/lucidcast/" rel="noopener noreferrer"&gt;Landing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OK, lessons.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. On-device Whisper is harder than it looks (NDK 29 patch needed)
&lt;/h2&gt;

&lt;p&gt;I wanted local transcription so audio doesn't leave the device.&lt;br&gt;
&lt;code&gt;whisper_ggml&lt;/code&gt; is the only Flutter binding I found that works&lt;br&gt;
end-to-end. Catch: it needs &lt;strong&gt;Android NDK 29.0.13113456&lt;/strong&gt; while&lt;br&gt;
most other plugins are still on 27. Setting &lt;code&gt;ndkVersion = "29..."&lt;/code&gt;&lt;br&gt;
in &lt;code&gt;android/app/build.gradle.kts&lt;/code&gt; works for new builds, but the&lt;br&gt;
plugin's auto-detected version was sometimes off — needed a small&lt;br&gt;
patch script (&lt;code&gt;flutter/tool/patch_whisper_ggml.sh&lt;/code&gt;) to enforce it&lt;br&gt;
during CI.&lt;/p&gt;

&lt;p&gt;Battery cost is real: I gate transcription to &lt;strong&gt;charging-only mode&lt;br&gt;
by default&lt;/strong&gt; with a configurable battery threshold (10-80 %, default&lt;br&gt;
50 %). When the user toggles "transcribe downloaded episodes," I&lt;br&gt;
queue the work but only execute when the phone is plugged in.&lt;br&gt;
Users on r/podcasts would otherwise notice a 5-10 % overnight&lt;br&gt;
battery drain.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sharing the microphone is a contract nobody documents
&lt;/h2&gt;

&lt;p&gt;Smart Pause uses &lt;code&gt;noise_meter&lt;/code&gt; to read ambient dB. Voice commands&lt;br&gt;
use Android's native &lt;code&gt;SpeechRecognizer&lt;/code&gt;. Both want exclusive access&lt;br&gt;
to &lt;code&gt;MediaRecorder.AudioSource.MIC&lt;/code&gt;, and if you don't coordinate&lt;br&gt;
them explicitly, the second consumer gets silent samples and every&lt;br&gt;
recognition cycle ends in &lt;code&gt;ERROR_NO_MATCH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: a centralised &lt;code&gt;NoiseDetectionService&lt;/code&gt; with a &lt;code&gt;block()&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;unblock()&lt;/code&gt; protocol. When the voice command service starts, it&lt;br&gt;
calls &lt;code&gt;block('voice_commands')&lt;/code&gt;, which cancels the noise meter's&lt;br&gt;
subscription &lt;strong&gt;and refuses any auto-restart&lt;/strong&gt;. On stop, &lt;code&gt;unblock()&lt;/code&gt;&lt;br&gt;
optionally restarts capture for the noise-dependent consumers.&lt;br&gt;
There's a 200 ms grace window between block and &lt;code&gt;SpeechRecognizer.&lt;br&gt;
startListening()&lt;/code&gt; so the platform finishes tearing down the previous&lt;br&gt;
&lt;code&gt;AudioRecord&lt;/code&gt;. Without that grace window, the bug returns&lt;br&gt;
intermittently.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. R8 resource shrinking + drawables-loaded-by-name = silent regressions
&lt;/h2&gt;

&lt;p&gt;This one cost me an entire release cycle. The &lt;code&gt;audio_service&lt;/code&gt;&lt;br&gt;
plugin lets you wire &lt;code&gt;MediaControl.custom&lt;/code&gt; actions with&lt;br&gt;
&lt;code&gt;androidIcon: 'drawable/ic_save_thought_aa'&lt;/code&gt;. At runtime, native&lt;br&gt;
code does &lt;code&gt;Resources.getIdentifier()&lt;/code&gt; to resolve the name — a&lt;br&gt;
&lt;strong&gt;dynamic lookup&lt;/strong&gt;, invisible to R8.&lt;/p&gt;

&lt;p&gt;In debug builds, every drawable ships, so it works. In release&lt;br&gt;
builds with &lt;code&gt;isShrinkResources = true&lt;/code&gt;, R8 sees no static reference&lt;br&gt;
to &lt;code&gt;ic_save_thought_aa.xml&lt;/code&gt;, prunes it, and at runtime&lt;br&gt;
&lt;code&gt;createCustomAction&lt;/code&gt; throws &lt;code&gt;IllegalArgumentException&lt;/code&gt;. That&lt;br&gt;
exception aborts the entire &lt;code&gt;setState()&lt;/code&gt; call. The &lt;code&gt;MediaSession&lt;/code&gt;&lt;br&gt;
stays at &lt;code&gt;state=NONE&lt;/code&gt;, the system never renders the playback&lt;br&gt;
notification or lock-screen controls, and Google Play eventually&lt;br&gt;
flags the app for not working in Android Auto.&lt;/p&gt;

&lt;p&gt;Fix: add a &lt;code&gt;res/raw/keep.xml&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;tools:keep="@drawable/ic_save_thought_aa"&lt;/code&gt;. Five lines. Caught&lt;br&gt;
because I had S25 + Edge running production-parity dumpsys&lt;br&gt;
comparisons after every release build.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Google Play reviewer tests every tier of your paywall
&lt;/h2&gt;

&lt;p&gt;I had a Pro-only tier gate on the Android Auto browse tree —&lt;br&gt;
&lt;code&gt;canBrowseCatalog()&lt;/code&gt; returned false for free-tier users, so&lt;br&gt;
&lt;code&gt;getChildren()&lt;/code&gt; short-circuited to an empty list. Reviewer (always&lt;br&gt;
free-tier, always fresh install) opened the app in Android Auto,&lt;br&gt;
saw "No items," and rejected the release with "App doesn't perform&lt;br&gt;
as expected — does not load any content after using the stop button."&lt;/p&gt;

&lt;p&gt;Lesson: &lt;strong&gt;anything Google's automated test rig can reach must work&lt;br&gt;
for free-tier users at the basic functional level&lt;/strong&gt;. Paywalls&lt;br&gt;
belong on premium features (AI summary, voice commands), not on&lt;br&gt;
hygiene features (browse, play, stop). I removed the gate entirely&lt;br&gt;
and let entity-level limits (max 3 subscriptions on free) do the&lt;br&gt;
differentiation organically.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Falling back to a 6 G heap on a 7.5 G dev box is not optional
&lt;/h2&gt;

&lt;p&gt;My dev server had 7.5 GB RAM. Gradle's default &lt;code&gt;-Xmx8G&lt;/code&gt; plus&lt;br&gt;
Kotlin compiler daemon's own &lt;code&gt;-Xmx8G&lt;/code&gt; plus the Flutter tool plus&lt;br&gt;
system overhead crashed mid-build with "Gradle build daemon&lt;br&gt;
disappeared unexpectedly" once swap saturated.&lt;/p&gt;

&lt;p&gt;Workaround: drop &lt;code&gt;org.gradle.jvmargs&lt;/code&gt; heap to &lt;code&gt;-Xmx6G&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;MaxMetaspaceSize=2G&lt;/code&gt;. Builds are 30 % slower but they finish.&lt;br&gt;
Long-term fix: bump the dev server RAM (which I eventually did).&lt;/p&gt;

&lt;p&gt;If you're on a 16 GB laptop, you're probably fine. If you're on&lt;br&gt;
an 8 GB VM, profile first.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Localisation is a Gemini batch script away
&lt;/h2&gt;

&lt;p&gt;The app ships in 22 languages. I wrote &lt;code&gt;flutter/tool/translate_arbs.py&lt;/code&gt;&lt;br&gt;
that takes &lt;code&gt;app_en.arb&lt;/code&gt; and produces 20 other ARBs via Gemini 2.5&lt;br&gt;
Flash. Two-pass approach: pass 1 fills missing keys, pass 2&lt;br&gt;
re-translates keys whose value still matches the English source&lt;br&gt;
(catches the "I forgot to localise this dialog" case after the&lt;br&gt;
fact). Heuristic skips short (&amp;lt;5 char) and uppercase-only strings&lt;br&gt;
so brand names and unit labels stay intact.&lt;/p&gt;

&lt;p&gt;Total cost for 22 languages: under $2 per re-translation run.&lt;br&gt;
Quality is good enough that I've only had to manually correct&lt;br&gt;
two locales after user feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'm at now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live on Google Play, v1.1.0+248&lt;/li&gt;
&lt;li&gt;~30 published reviews on the previous build cycle, fresh review
reset on the new version&lt;/li&gt;
&lt;li&gt;Solo founder, no investors, EU privacy-first positioning&lt;/li&gt;
&lt;li&gt;Looking for honest feedback from indie devs and Android power-users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped an indie Android app in the past two years, I'd&lt;br&gt;
love to hear what your post-launch first-30-days playbook was.&lt;br&gt;
Discovery on Play Store is the part I'm finding hardest — no&lt;br&gt;
ASO budget, no influencer relationships, no paid acquisition until&lt;br&gt;
the listing has 50+ reviews to convert.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=cz.prismatic.lucidcast" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;&lt;br&gt;
| &lt;a href="https://www.prismatic.cz/lucidcast/" rel="noopener noreferrer"&gt;Landing page&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Early tester program (DEV.to readers)
&lt;/h2&gt;

&lt;p&gt;If you're going to actually use Lucidcast, I'm giving out &lt;strong&gt;20 free&lt;br&gt;
90-day AI Pack trial codes&lt;/strong&gt; to DEV.to readers. Drop me a DM if you&lt;br&gt;
want one.&lt;/p&gt;

&lt;p&gt;The honest mechanics, because nobody likes surprises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code gives you 90 days of AI Pack (on-device Whisper + AI
summaries) for free&lt;/li&gt;
&lt;li&gt;After 90 days, the subscription auto-renews at the normal price
(~€3/month) unless you cancel — Google Play lets you cancel any
time from Play Store → Subscriptions&lt;/li&gt;
&lt;li&gt;Why 90 days, not lifetime: Google Play caps subscription promo
codes at 90 days. If you want a fresh 90 days when this runs out,
DM me — happy to send another code as long as you've been using
the app and willing to share occasional feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be explicit because Play Store policy matters: &lt;strong&gt;the code is for&lt;br&gt;
feedback to me directly, not for a Play Store review.&lt;/strong&gt; I can't tie&lt;br&gt;
the two for compliance reasons and won't ask you to. If you happen&lt;br&gt;
to leave a review separately because you want to, that's your call,&lt;br&gt;
not part of the deal.&lt;/p&gt;

&lt;p&gt;How it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DM me the Google account email you use for Play Store&lt;/li&gt;
&lt;li&gt;I generate a promo code and send it back&lt;/li&gt;
&lt;li&gt;You redeem it in the app or via Play Store; AI Pack unlocks for
90 days&lt;/li&gt;
&lt;li&gt;After ~1 week of use, send me a short DM with what worked, what
didn't, what's missing&lt;/li&gt;
&lt;li&gt;When 90 days runs out, either let it auto-renew (you decided you
like it), cancel, or ping me for a fresh code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First 20 takers. Czech indie can't comp the world but can comp the&lt;br&gt;
early DEV.to crowd.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Comments and bug reports are open.&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
