Skip to content

feat: add language selector for Whisper transcription#543

Open
data-bot-coasys wants to merge 13 commits intodevfrom
feature/transcription-language-selector
Open

feat: add language selector for Whisper transcription#543
data-bot-coasys wants to merge 13 commits intodevfrom
feature/transcription-language-selector

Conversation

@data-bot-coasys
Copy link
Contributor

@data-bot-coasys data-bot-coasys commented Feb 22, 2026

Summary

Adds a language selector dropdown to the Transcriber widget, allowing users to choose the transcription language for Whisper.

Changes

  • Added a language dropdown (English, German) near the Whisper model status area in TranscriberWidget.vue
  • Selected language is passed as a 4th argument to openTranscriptionStream calls
  • Stored in a reactive ref, defaulting to English

Note

This PR works alongside the upcoming AD4M PR that adds language parameter support to openTranscriptionStream. The AD4M client method signature will be updated to accept the language parameter in that separate PR.

Summary by CodeRabbit

  • New Features

    • Language selector added to transcription UI (English, German); default set to English.
    • Selected language is applied to live transcription streams and preserved across restarts.
  • Bug Fixes

    • Changing language while listening now reliably restarts transcription automatically.
    • Restart behavior improved to avoid race conditions when switching microphones or service settings.

Add a language dropdown (English/German) to the transcriber widget.
The selected language is passed to openTranscriptionStream calls.
Works with the upcoming AD4M language parameter support.
@netlify
Copy link

netlify bot commented Feb 22, 2026

Deploy Preview for fluxdocs ready!

Name Link
🔨 Latest commit 0a70366
🔍 Latest deploy log https://app.netlify.com/projects/fluxdocs/deploys/69a044ee07c8d90008e9fa12
😎 Deploy Preview https://deploy-preview-543--fluxdocs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 22, 2026

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit fc3a942
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/69ad9f8ee5813c000827649e
😎 Deploy Preview https://deploy-preview-543--fluxsocial-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a LanguageSelector component and a reactive selectedLanguage to TranscriberWidget; the widget passes the language into transcription stream initialization and restarts listening when language or microphone/service state changes. Also updates package resolutions for ad4m packages to a whisper-language dev version.

Changes

Cohort / File(s) Summary
Transcriber Widget
app/src/components/call/widgets/TranscriberWidget.vue
Adds LanguageSelector UI, introduces reactive selectedLanguage (default "en"), passes it to both openTranscriptionStream calls, makes restartListening await stopListening, and updates watchers to asynchronously restart listening when language, remote service, or active microphone change.
New Component
app/src/components/call/widgets/LanguageSelector.vue
New Vue 3 SFC with prop modelValue: string and update:modelValue event; renders a select offering English (en) and German (de).
Dependency Resolutions
package.json
Updates resolutions["@coasys/ad4m"] and resolutions["@coasys/ad4m-connect"] from 0.11.2-dev.5 to 0.12.0-dev-whisper-lang.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant UI as TranscriberWidget
participant Mic as Microphone
participant TS as TranscriptionService
User->>UI: choose language (en/de)
UI->>UI: set selectedLanguage (reactive)
alt listening active
UI->>UI: await stopListening()
UI->>Mic: stop capture
UI->>TS: close current transcription stream
UI->>TS: openTranscriptionStream(selectedLanguage)
UI->>Mic: start capture
TS-->>UI: transcription results
UI-->>User: update transcript
else not listening
UI->>TS: store language for next session
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I twitch my whiskers, choose "en" or "de",

I pause the stream, then wake it gently.
Words hop back in a bilingual queue,
Soft transcripts sprout — a carrot or two.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a language selector component for Whisper transcription, which is the primary feature across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/transcription-language-selector

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

data-bot-coasys and others added 3 commits February 22, 2026 17:25
Watches selectedLanguage and calls restartListening() to close the
current stream and open a new one with the updated language parameter.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/call/widgets/TranscriberWidget.vue`:
- Line 124: The select bound to v-model="selectedLanguage" in
TranscriberWidget.vue lacks an accessible label; add an explicit accessible name
by either adding an aria-label (e.g., aria-label="Language") directly to the
<select> with v-model="selectedLanguage" or give the <select> an id and pair it
with the existing <j-text> by converting that element into a <label for="...">
so screen readers can announce the control correctly.
- Around line 122-128: The language <select> is rendered only in the v-else
(when modelsReady is false) so it disappears once modelsReady becomes true; move
or duplicate the selector so it's available during active transcription (e.g.,
place it inside the v-if="modelsReady" branch within the <j-box mb="300"> header
area) so users can change selectedLanguage and trigger the existing watcher that
calls restartListening(); ensure you update the template around the modelsReady
block rather than only the loading branch and keep v-model="selectedLanguage"
unchanged.
- Around line 619-623: The restartListening race occurs because
restartListening() calls stopListening() without awaiting it, letting
stopListening's delayed null assignments clobber newly-written stream IDs; make
restartListening an async function and call await stopListening() before calling
startListening(), and update all watchers that call restartListening (the
selectedLanguage watcher, the useRemoteService watcher, and the
activeMicrophoneId watcher) to use async arrow handlers that await
restartListening() so the stop→start sequence completes serially and prevents
streamId/fastStreamId from being nulled after new streams are created.

Comment on lines +619 to +623
watch(selectedLanguage, () => {
if (listening.value) {
restartListening();
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Watcher relies on restartListening() which races stopListening() — stream IDs may be nulled after new streams open

restartListening() (line 597) calls stopListening() without await and then immediately calls startListening(). stopListening sets streamId.value = null / fastStreamId.value = null only after its closeTranscriptionStream awaits. If those awaits complete after startListening has already written new stream IDs, the old null assignments clobber them — leaving the old streams leaked and the new ones un-closeable. This pre-exists for mic/remote-service restarts; the new language watcher now adds a third trigger for the same race.

♻️ Suggested fix — make `restartListening` async and await `stopListening`
-function restartListening() {
+async function restartListening() {
   console.log('Restarting listening with new settings');
-  stopListening();
+  await stopListening();
   startListening();
 }

Also update the watcher to await the call:

 watch(selectedLanguage, () => {
   if (listening.value) {
-    restartListening();
+    void restartListening();
   }
 });

(The other two call sites — watch(useRemoteService, ...) and watch(activeMicrophoneId, restartListening) — should similarly be updated to async () => { await restartListening(); } to keep them consistent.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watch(selectedLanguage, () => {
if (listening.value) {
restartListening();
}
});
watch(selectedLanguage, () => {
if (listening.value) {
void restartListening();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/widgets/TranscriberWidget.vue` around lines 619 -
623, The restartListening race occurs because restartListening() calls
stopListening() without awaiting it, letting stopListening's delayed null
assignments clobber newly-written stream IDs; make restartListening an async
function and call await stopListening() before calling startListening(), and
update all watchers that call restartListening (the selectedLanguage watcher,
the useRemoteService watcher, and the activeMicrophoneId watcher) to use async
arrow handlers that await restartListening() so the stop→start sequence
completes serially and prevents streamId/fastStreamId from being nulled after
new streams are created.

- Move language selector into modelsReady branch so it's accessible during active transcription
- Add aria-label to both select elements for accessibility
- Make restartListening() async and await stopListening() to prevent stream ID race condition
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/src/components/call/widgets/TranscriberWidget.vue (2)

626-639: Watchers look correct; minor guard inconsistency on activeMicrophoneId.

The selectedLanguage (line 626) and useRemoteService (line 633) watchers are properly guarded and async. One nit: the activeMicrophoneId watcher (line 639) restarts unconditionally — it lacks the listening.value guard that the other two watchers use, so a microphone change while audio is muted will unexpectedly start transcription. This behavior predates this PR but the watcher was touched as part of the async update, making it a good moment to align.

Also, async () => await restartListening() can be simplified to () => restartListening() — both return a Promise to Vue.

♻️ Suggested consistency fix for activeMicrophoneId watcher
-watch(activeMicrophoneId, async () => await restartListening());
+watch(activeMicrophoneId, async () => {
+  if (listening.value) await restartListening();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/widgets/TranscriberWidget.vue` around lines 626 -
639, The activeMicrophoneId watcher should match the guard and async style of
the others: only call restartListening() when listening.value is true to avoid
starting transcription while muted, and simplify the arrow by removing the
unnecessary async/await (i.e., change the watcher callback to check
listening.value then call restartListening()). Update the watcher referencing
activeMicrophoneId and the restartListening invocation accordingly, keeping the
existing watchers for selectedLanguage and useRemoteService as the model.

129-135: Consider extracting the duplicated language <select> into a small component.

The selector markup is identical across both template branches (lines 52–58 and 129–135). If options or styling ever change, both need to be updated in sync. Extracting to a single LanguageSelector.vue (or similar) eliminates the duplication.

♻️ Suggested extraction
// LanguageSelector.vue (new file)
+<template>
+  <j-flex gap="300" a="center">
+    <j-text nomargin weight="800" size="400">Language:</j-text>
+    <select
+      :value="modelValue"
+      aria-label="Transcription language"
+      style="background: var(--j-color-ui-100); color: var(--j-color-ui-800); border: 1px solid var(--j-color-ui-300); border-radius: 4px; padding: 2px 6px; font-size: 12px;"
+      `@change`="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
+    >
+      <option value="en">English</option>
+      <option value="de">German</option>
+    </select>
+  </j-flex>
+</template>
+<script setup lang="ts">
+defineProps<{ modelValue: string }>();
+defineEmits<{ 'update:modelValue': [value: string] }>();
+</script>

Then in TranscriberWidget.vue, replace both occurrences with:

-<j-flex gap="300" a="center">
-  <j-text nomargin weight="800" size="400">Language:</j-text>
-  <select v-model="selectedLanguage" aria-label="Transcription language" style="...">
-    <option value="en">English</option>
-    <option value="de">German</option>
-  </select>
-</j-flex>
+<LanguageSelector v-model="selectedLanguage" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/widgets/TranscriberWidget.vue` around lines 129 -
135, Extract the duplicated <select> into a small reusable component (e.g.,
LanguageSelector.vue) that encapsulates the options and styling and supports
v-model binding (implement modelValue prop and emit update:modelValue) so it can
bind to the existing selectedLanguage data in TranscriberWidget.vue; then
replace both inline select blocks in TranscriberWidget.vue with
<LanguageSelector v-model="selectedLanguage" /> (or the equivalent tag) to
remove duplication and centralize future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/components/call/widgets/TranscriberWidget.vue`:
- Around line 604-608: The restartListening function had a race where
startListening could run before stopListening finished clearing stream state;
ensure restartListening remains async and awaits stopListening() before calling
startListening() so streamId and fastStreamId are nulled first—keep the current
implementation in restartListening(), and reference stopListening() and
startListening() to locate the fix.

---

Nitpick comments:
In `@app/src/components/call/widgets/TranscriberWidget.vue`:
- Around line 626-639: The activeMicrophoneId watcher should match the guard and
async style of the others: only call restartListening() when listening.value is
true to avoid starting transcription while muted, and simplify the arrow by
removing the unnecessary async/await (i.e., change the watcher callback to check
listening.value then call restartListening()). Update the watcher referencing
activeMicrophoneId and the restartListening invocation accordingly, keeping the
existing watchers for selectedLanguage and useRemoteService as the model.
- Around line 129-135: Extract the duplicated <select> into a small reusable
component (e.g., LanguageSelector.vue) that encapsulates the options and styling
and supports v-model binding (implement modelValue prop and emit
update:modelValue) so it can bind to the existing selectedLanguage data in
TranscriberWidget.vue; then replace both inline select blocks in
TranscriberWidget.vue with <LanguageSelector v-model="selectedLanguage" /> (or
the equivalent tag) to remove duplication and centralize future changes.

…ent and fix watcher

- Extract duplicated language <select> into reusable LanguageSelector.vue component
- Replace both inline select blocks with <LanguageSelector v-model>
- Fix activeMicrophoneId watcher to only restart when listening (matches other watchers)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/src/components/call/widgets/LanguageSelector.vue (1)

6-6: Consider moving inline styles to scoped CSS.

The inline style string is long and will be harder to theme/override than a scoped CSS class. Extracting it keeps the template readable.

♻️ Proposed refactor
-  <select
-    :value="modelValue"
-    `@change`="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
-    aria-label="Transcription language"
-    style="background: var(--j-color-ui-100); color: var(--j-color-ui-800); border: 1px solid var(--j-color-ui-300); border-radius: 4px; padding: 2px 6px; font-size: 12px;"
-  >
+  <select
+    :value="modelValue"
+    `@change`="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
+    aria-label="Transcription language"
+    class="language-select"
+  >

Then add to the <style scoped> block (create one if it doesn't exist):

+<style scoped>
+.language-select {
+  background: var(--j-color-ui-100);
+  color: var(--j-color-ui-800);
+  border: 1px solid var(--j-color-ui-300);
+  border-radius: 4px;
+  padding: 2px 6px;
+  font-size: 12px;
+}
+</style>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/widgets/LanguageSelector.vue` at line 6, The element
in the LanguageSelector component currently uses a long inline style string;
extract that inline style into a scoped CSS class (e.g., .language-badge) inside
a <style scoped> block in the LanguageSelector.vue file, replace the style
attribute on the element with the new class, and ensure the class contains the
rules: background, color, border, border-radius, padding and font-size so
theming/overrides work cleanly.
app/src/components/call/widgets/TranscriberWidget.vue (1)

600-600: Remove the debug console.log.

This fires on every language change, microphone switch, and service toggle — it will be noisy in production.

🐛 Proposed fix
 async function restartListening() {
-  console.log('Restarting listening with new settings');
   await stopListening();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/call/widgets/TranscriberWidget.vue` at line 600, Remove
the noisy debug console.log by deleting the statement that prints "Restarting
listening with new settings" in TranscriberWidget.vue ( locate the string in the
method that restarts or re-initializes the transcriber - e.g., the
restart/listen handler where language changes, mic switches, or service toggles
are handled ); if you want to keep optional debug output, replace it with a
conditional/verbosity-controlled logger call (e.g., use an existing logger.debug
or guard it behind a development-only flag) instead of console.log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/components/call/widgets/LanguageSelector.vue`:
- Line 6: The element in the LanguageSelector component currently uses a long
inline style string; extract that inline style into a scoped CSS class (e.g.,
.language-badge) inside a <style scoped> block in the LanguageSelector.vue file,
replace the style attribute on the element with the new class, and ensure the
class contains the rules: background, color, border, border-radius, padding and
font-size so theming/overrides work cleanly.

In `@app/src/components/call/widgets/TranscriberWidget.vue`:
- Line 600: Remove the noisy debug console.log by deleting the statement that
prints "Restarting listening with new settings" in TranscriberWidget.vue (
locate the string in the method that restarts or re-initializes the transcriber
- e.g., the restart/listen handler where language changes, mic switches, or
service toggles are handled ); if you want to keep optional debug output,
replace it with a conditional/verbosity-controlled logger call (e.g., use an
existing logger.debug or guard it behind a development-only flag) instead of
console.log.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd204a and acf8e5c.

📒 Files selected for processing (2)
  • app/src/components/call/widgets/LanguageSelector.vue
  • app/src/components/call/widgets/TranscriberWidget.vue

lucksus and others added 3 commits February 24, 2026 01:06
Grouped into Common, European, Asian, African, and Other categories
using optgroup for better navigation. All ISO 639-1 codes match
the kalosm WhisperLanguage enum used by AD4M.
distil-large-v3 is English-only, causing non-English language selection
to have no effect on the main transcription stream. Switch to the full
whisper_large_v2 model which supports all 99 languages.

Note: kalosm doesn't have a non-distilled LargeV3 variant, so LargeV2
is the best available full multilingual model.
@data-bot-coasys data-bot-coasys force-pushed the feature/transcription-language-selector branch from dbc5b85 to 8eec1e2 Compare February 24, 2026 00:35
Address CodeRabbit review: stopListening nulls stream IDs, so startListening
must be awaited to prevent race where new streams open before cleanup completes.
- Move inline styles to scoped CSS class in LanguageSelector
- Remove debug console.log from restartListening
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