feat: Indonesian locale, Telegram Scan all, YouTube browser, and offline Transcript - #291
feat: Indonesian locale, Telegram Scan all, YouTube browser, and offline Transcript#291harezadmm wants to merge 5 commits into
Conversation
…ure keys Adds a full Indonesian (id.json) translation baseline sourced from en.json with UI-critical strings translated to Bahasa Indonesia. Registers the locale in the loaders list and adds it to the language selectors on Settings and Onboarding. The onboarding selector was also missing zh-TW and ru; both are added here for parity with Settings. Also adds shared translation keys used by follow-up commits: - ytdlp_missing_banner (fixes raw-key banner shown at app top) - telegram: manage / manage_accounts / manage_channel / sync_* / scan_* - youtube: full section for the new YouTube page (all + tabs + actions) - transcript: full section for the new Transcript page Broadcasts keys across all 11 locales so pnpm generate:i18n-keys reports "all locales in sync with en.json".
Adds a "Scan all" action next to "Download all" that exhaustively lists every media type (photo, video, document, audio) in the currently open chat. The four types are paginated in parallel via Promise.all, and mediaItems is left untouched during the scan (results are collected in a Map by message_id and assigned once at the end) so the media-list does not re-render on every page — avoiding UI jank on large channels. After the scan completes the result is cached per chat. Subsequent tab switches (All/Photos/Videos/Files/Audio) filter locally against the cache instead of re-querying the plugin. The cache is invalidated when the user picks a different chat or returns to the chat list. Progress is shown as an indeterminate bar plus per-type chips (Photos · N · Videos · N · Files · N · Audio · N) and a total counter, with a Cancel button that stops the in-flight pagination cleanly. Also replaces the remaining hardcoded Portuguese strings on the Telegram surface (Gerenciar buttons/aria-labels, Sync pendente, Sincronizando, "há Nh"/"há Nd" ago labels, tooltips) with $t() calls so an English user actually sees English throughout the plugin.
New /youtube route that accepts a channel handle (/@x, /channel/, /c/, /user/), playlist or single-video URL. For channels the page shows the familiar YouTube tab layout (Home / Videos / Shorts / Live / Playlists) and constructs the corresponding sub-URL when a tab is opened. Results are cached in memory keyed by (url, limit) so switching between tabs is instant on the second visit. Each entry renders with its YouTube thumbnail derived from the video ID (https://i.ytimg.com/vi/<id>/mqdefault.jpg) with loading="lazy" so a 1000-item list does not fire a thousand simultaneous requests. To keep the initial fetch fast on large channels the frontend requests only the first 200 entries by default. A "Load full channel" button appears when the partial cap is hit and re-issues the same call with no limit. This required extending the playlist_entries Tauri command with an optional `limit: Option<u32>` parameter that passes `--playlist-end N` through to yt-dlp; when unset the behaviour is identical to before. Multi-select is available with Select all / Clear / Invert. Downloads are queued through the existing download_from_url command one URL at a time so users can watch progress in the Downloads page. Also registers the YouTube entry in nav-config so it shows up under the Plugins group in the sidebar.
New /transcript route that transcribes local video/audio files to plain
text via faster-whisper (Python), with an in-page converter that turns
the resulting transcript into a Markdown document.
Backend (Tauri commands, all in src/commands/transcript.rs):
- transcribe_video(video_path, output_path, model_size, language,
job_id): spawns Python running an embedded faster-whisper script,
streams line-delimited JSON stages back over the Tauri event bus
("transcript:{jobId}") so the UI can render loading_model /
transcribing / progress {percent, current, total} / done / error
without polling. Emits the final transcript path and detected
language once complete.
- check_whisper_installed(): probes whether Python can import
faster_whisper so the UI can show an install hint instead of a
cryptic runtime error.
- write_text_file(path, content): small helper used by the Markdown
side of the page to persist the generated .md.
The Python script is embedded as a Rust constant and dropped into the
OS temp dir on first use to avoid a separate resource file.
Frontend:
- File picker for the source media (mp4/mkv/mov/webm/mp3/wav/…).
- Model size selector: tiny/base/small (recommended)/medium/large-v3
with rough download-size hints.
- Language selector including Bahasa Indonesia and Auto-detect.
- Live progress bar and per-segment percentage/time counter driven
by the event stream.
- Result textarea plus Copy and "Send to Markdown converter" action.
- Markdown converter section that adds an H1 title and groups lines
into evenly-sized paragraphs, with Copy and Save as .md actions.
Registers three new commands in lib.rs, adds a Transcript entry to
nav-config under the Plugins group, and gates the whole page with a
warning banner + install command when faster-whisper is missing so a
first-run user knows exactly what to install.
tonhowtf
left a comment
There was a problem hiding this comment.
Thank you for this — the Indonesian locale in particular is real work and I want it. But I can't take the branch as one unit, and the reason isn't the size: one of the four features ships a command that lets any code in the webview write to any path on disk. Details below, ordered by how much they matter.
I did merge and build it before writing this, so none of the following is guessed: cargo check --workspace finishes clean, pnpm check reports 0 errors / 107 warnings — the same baseline as main. It compiles. That isn't the problem.
1. write_text_file is an unrestricted arbitrary file write — blocking
#[tauri::command]
pub async fn write_text_file(path: String, content: String) -> Result<(), String> {
tokio::fs::write(&path, content).await
}It's registered globally in generate_handler! (lib.rs:895), so it isn't scoped to the Transcript page — it's reachable from every surface in the app, including the plugin frontends, which run in the same webview. There is no path validation, no directory confinement, no extension check. invoke("write_text_file", { path: "/Users/me/.zshrc", content: "..." }) succeeds.
OmniGet renders remote-derived content (titles, descriptions, thumbnails, plugin-supplied strings), so "only our own code calls it" isn't a boundary I want to lean on for a primitive this strong. The transcript is already written to disk by the Python side, and Tauri's dialog plugin — already a dependency — gives you a save-path the user picked. Please drop the command and route the Markdown export through that.
2. The transcription duplicates whisper_generate, and adds a Python runtime to do it
commands/ai.rs:213 already exposes whisper_generate, backed by omniget_core::core::ai::transcribe. The new command is a second, unrelated transcription engine, so the app would ship two answers to one question with no shared config, history, or error handling — note the existing one calls ai::history_add("transcript", ...), which this path doesn't participate in.
The bigger cost is the dependency. OmniGet manages its binaries (yt-dlp, ffmpeg, deno, aria2c) — it downloads them, versions them, repairs them, and reports their status in Settings. faster-whisper fits none of that: it needs a Python 3.10+ interpreter on PATH, a pip install --user, and a model download on first run. "The page shows a banner when it's missing" makes the failure legible; it doesn't make it opt-in, because the nav item is always visible and there's no way to act on the banner from inside the app.
If local CPU transcription is worth having — and I think it is, it's the honest answer for users without an API key — the shape should be a whisper.cpp/whisper-rs binary in the managed-dependency table, feeding the existing transcription command as a local provider. That's a design conversation worth having in an issue first, and it's a different PR.
3. The Python subprocess deadlocks on first run
.stdout(Stdio::piped())
.stderr(Stdio::piped())stderr is piped and then never read. The loop drains stdout to EOF and only then calls child.wait(). Once the child writes more than the pipe buffer (64 KB on Linux) to stderr, it blocks in write() forever: it can't progress, so stdout never reaches EOF, so nothing ever drains stderr. Classic deadlock, and no timeout to break it.
This isn't a corner case — it's the first-run path. WhisperModel(model_size, ...) downloads the model through huggingface_hub, whose progress bars go to stderr, and ctranslate2 emits its warnings there too. A model download blows past 64 KB comfortably. Fix is either Stdio::null() for stderr, or draining it concurrently with tokio::join! (worth keeping — those warnings are exactly what you'd want in a bug report).
Two smaller things in the same function:
- The script goes to a fixed, predictable path:
std::env::temp_dir().join("omniget_transcribe.py"). On Linux that's a shared, world-writable/tmp, so another local user can pre-create that name as a symlink and have us write through it — or win the race between our write and the spawn and get their Python executed by our process. macOS is fine here (per-userTMPDIR), Linux is not. Use a randomized directory, orinclude_str!the script and pass it viastdin. - There's no cancellation. Navigating away leaves the child running, and a long transcription can't be stopped.
4. /youtube and /transcript are added to CORE_NAV_ITEMS as group: "plugins"
{ href: "/youtube", labelKey: "nav.youtube", icon: "plugin", group: "plugins", order: 60 },
{ href: "/transcript", labelKey: "nav.transcript", icon: "plugin", group: "plugins", order: 65 },The plugins group means "appears when the plugin is installed", and entries in it carry a pluginId. These have none, so they render in the plugins section unconditionally, with the generic plugin icon, while not being plugins. That takes the core sidebar from 5 items to 7 — in the cycle where the whole point has been to cut, not add.
The /youtube page itself is decent work and I don't want to lose it, but a channel browser is arguably the omnibox's job when a channel URL is pasted, rather than a permanent seventh nav entry. Worth its own PR and its own conversation.
What I'd merge today
Split this and I'll take these two immediately:
feat(i18n)—id.json, the selector exposure forid/zh-TW/ru(those last two being missing is a straightforward bug), and theytdlp_missing_banner.textkeys. Clean, self-contained, obviously good.feat(telegram)— Scan all with the parallel per-type listing and per-chat cache, plus killing the last hardcoded Portuguese (Gerenciar,Sync pendente,Sincronizando, thehá Nh/há Ndlabels). Also good.
The playlist_entries(url, limit: Option<u32>) change is fine and backward-compatible — Option deserializes from a missing argument, and src/routes/+page.svelte:463 is the only other caller. Send it along with whichever half needs it.
These two need a different shape before I can take them: transcript (as #1–#3 above) and the /youtube nav placement (#4).
To be clear about why I'm splitting rather than asking for fixes in place: with four features in one branch, the i18n work — which is ready — can't ship until the transcript security question is settled. Separating them is what gets your Indonesian locale into the next release instead of blocking it behind a design discussion.
|
Thank you for this — four features, clean commit separation, and a PR description that actually explains the tradeoffs is more care than most contributions get. I want to get most of this in. But it needs to go in as separate PRs, and a couple of things need fixing first. My review from 2026-08-17 still stands in full (the 1. Indonesian locale — needs the actual translationThe mechanics are right: The problem is the values. 122 of 3989 strings (3.1%) are actually Indonesian. The other 3867 are byte-identical to For comparison, To make this mergeable: translate the core surfaces at minimum — Two pieces of this commit are ready today, and I'm taking them now rather than making them wait behind the rest — credited to you in the changelog:
2. Telegram "Scan all" — closest to readyI checked the thing I was most worried about: this needs no change in Four things before I take it:
One next door that you didn't touch: 3. YouTube browser — good code, wrong place in the nav, broken CSS tokensThe fetching approach is exactly right and worth saying so: no new dependency, no embedded webview, no scraping. It goes through the existing Blocking:
Non-blocking: for a single-video link the entry title is the raw URL ( 4. Offline Transcript — I can't take this oneThis is where the answer is no rather than not-yet, and I want to explain rather than just decline. My 2026-08-17 points on Adding to it:
On licensing, for the record: nothing is vendored, and faster-whisper, CTranslate2 and the Whisper weights are all MIT, so there is no conflict with GPL-3. The objection is that this makes a Python 3.10+ interpreter, a If you want to pursue local transcription — and I think it is the right answer for users without an API key — the shape I'd say yes to is a HousekeepingPlease drop What I'd likeFour PRs instead of one:
I'm splitting rather than asking for fixes in place because right now a bug users are actively hitting can't ship until a security question about a different feature is resolved. Separating them is what gets your work in sooner, not later. Happy to review each as it lands. |
The yt-dlp missing banner rendered its own key names — ytdlp_missing_banner.text with a button reading ytdlp_missing_banner.open_settings — because the keys were in no locale at all. zh-TW shipped as a locale but appeared in neither language selector, and ru was missing from the onboarding one. Reported by @harezadmm in #291.
Summary
Four related additions bundled together because the i18n changes underneath are shared. Split into four Conventional Commits for reviewability:
id.json) as a first-class locale, exposes it in the Settings + Onboarding language selectors alongside the previously missingzh-TWandruoptions, and adds the shared translation keys used by the follow-up features. Also fixes the rawytdlp_missing_banner.textbanner (the keys were never in any locale).Promise.all, caches the result per chat, and serves subsequent tab switches from the cache instead of re-querying. Media list is not touched during the scan so large channels no longer jank the UI. Also replaces the last hardcoded Portuguese strings on the Telegram surface (Gerenciar,Sync pendente,Sincronizando, "há Nh"/"há Nd" ago labels) with$t()calls./youtuberoute accepting a channel handle (/@x,/channel/,/c/,/user/), playlist or single-video URL. For channels it shows the familiar tab layout (Home / Videos / Shorts / Live / Playlists), fetches on demand, caches per (url, limit). Each entry renders with its YouTube thumbnail (i.ytimg.com/vi/<id>/mqdefault.jpg,loading=\"lazy\"). Initial fetch is capped at 200 entries with a Load full channel button; this required a small backend change —playlist_entriesnow accepts an optionallimit: Option<u32>that is forwarded as--playlist-end Nto yt-dlp. When unset the behaviour is identical to before./transcriptroute that transcribes local video/audio files to plain text viafaster-whisper(Python) with an in-page converter that turns the transcript into Markdown. Backend spawns Python running an embedded script and streams line-delimited JSON stages back over the Tauri event bus (transcript:{jobId}) so the UI can render loading_model / transcribing / progress / done without polling. Frontend gates the page with a warning banner + install command whenfaster-whisperisn't importable, so first-run users know exactly what to install.Files changed
src/lib/i18n/id.json,src/routes/youtube/+page.svelte,src/routes/transcript/+page.svelte,src-tauri/src/commands/transcript.rskeys.ts,nav-config.ts, language selectors,playlist_entriescommandwhichwas already inCargo.tomlExternal runtime deps
faster-whisper(Python) for the transcript feature — user installs withpip install --user faster-whisper. The Transcript page detects the absence and shows install instructions, so this is opt-in per user.Test plan
pnpm check— passes with 0 errors (baseline warnings unchanged, 107)pnpm generate:i18n-keys— reports all 10 non-en locales in sync with en.jsoncargo fmt --all— cleancargo clippy --workspace --all-targets— expected clean; CI will confirmfaster-whisperinstalled, transcribe a 30s clip; without it, verify the warning banner showsKnown limitations / follow-ups
playlist_entries(yt-dlp) — video duration and view count are not returned by--flat-playlistso they're not shown yet.🤖 Generated with Claude Code