feat: add language selector for Whisper transcription#543
feat: add language selector for Whisper transcription#543data-bot-coasys wants to merge 13 commits intodevfrom
Conversation
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.
✅ Deploy Preview for fluxdocs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for fluxsocial-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a LanguageSelector component and a reactive Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Watches selectedLanguage and calls restartListening() to close the current stream and open a new one with the updated language parameter.
There was a problem hiding this comment.
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.
| watch(selectedLanguage, () => { | ||
| if (listening.value) { | ||
| restartListening(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| 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
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/src/components/call/widgets/TranscriberWidget.vue (2)
626-639: Watchers look correct; minor guard inconsistency onactiveMicrophoneId.The
selectedLanguage(line 626) anduseRemoteService(line 633) watchers are properly guarded and async. One nit: theactiveMicrophoneIdwatcher (line 639) restarts unconditionally — it lacks thelistening.valueguard 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
activeMicrophoneIdwatcher-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)
There was a problem hiding this comment.
🧹 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 debugconsole.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
📒 Files selected for processing (2)
app/src/components/call/widgets/LanguageSelector.vueapp/src/components/call/widgets/TranscriberWidget.vue
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.
dbc5b85 to
8eec1e2
Compare
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
Summary
Adds a language selector dropdown to the Transcriber widget, allowing users to choose the transcription language for Whisper.
Changes
TranscriberWidget.vueopenTranscriptionStreamcallsNote
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
Bug Fixes