auto-git:

[add] dist/assets/index-DifKT69O.js
 [change] backend/main.py
 [change] backend/schemas.py
 [change] backend/whisper_admin.py
 [change] dist/index.html
 [change] electron/main.cjs
 [change] src/App.jsx
 [change] src/GeneralSettings.jsx
 [change] src/audioInput.js
 [unlink] dist/assets/index-Dm7DZNSo.js
This commit is contained in:
2026-04-17 04:43:28 +02:00
parent 67d9f41f17
commit 48c35698cb
9 changed files with 85 additions and 14 deletions

View File

@@ -159,6 +159,7 @@ async def transcribe_audio_route(req: schemas.AudioTranscriptionRequest):
audio_bytes, audio_bytes,
mime_type, mime_type,
req.model or DEFAULT_WHISPER_MODEL, req.model or DEFAULT_WHISPER_MODEL,
req.language,
) )
except RuntimeError as exc: except RuntimeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc

View File

@@ -84,6 +84,7 @@ class AudioTranscriptionRequest(BaseModel):
mime_type: str mime_type: str
audio_base64: str audio_base64: str
model: Optional[str] = None model: Optional[str] = None
language: Optional[str] = None
class AudioTranscriptionResponse(BaseModel): class AudioTranscriptionResponse(BaseModel):

View File

@@ -241,6 +241,7 @@ def transcribe_audio_bytes(
audio_bytes: bytes, audio_bytes: bytes,
mime_type: str, mime_type: str,
model_name: str = DEFAULT_WHISPER_MODEL, model_name: str = DEFAULT_WHISPER_MODEL,
language: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
if not audio_bytes: if not audio_bytes:
raise RuntimeError("Recorded audio was empty.") raise RuntimeError("Recorded audio was empty.")
@@ -256,7 +257,14 @@ def transcribe_audio_bytes(
_convert_audio_to_wav(input_path, wav_path) _convert_audio_to_wav(input_path, wav_path)
source_path = wav_path source_path = wav_path
result = model.transcribe(str(source_path), task="transcribe", fp16=device == "cuda") transcription_options = {
"task": "transcribe",
"fp16": device == "cuda",
}
if language:
transcription_options["language"] = str(language).strip().lower()
result = model.transcribe(str(source_path), **transcription_options)
return { return {
"model": model_name, "model": model_name,
"device": device, "device": device,

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LLM Desktop</title> <title>LLM Desktop</title>
<script type="module" crossorigin src="/assets/index-Dm7DZNSo.js"></script> <script type="module" crossorigin src="/assets/index-DifKT69O.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BFOWWhCS.css"> <link rel="stylesheet" crossorigin href="/assets/index-BFOWWhCS.css">
</head> </head>
<body> <body>

View File

@@ -28,6 +28,7 @@ const MAX_UI_SCALE = 1.3
const DEFAULT_OPEN_DEVTOOLS_ON_STARTUP = false const DEFAULT_OPEN_DEVTOOLS_ON_STARTUP = false
const DEFAULT_AUDIO_INPUT_ENABLED = true const DEFAULT_AUDIO_INPUT_ENABLED = true
const DEFAULT_AUDIO_INPUT_DEVICE_ID = '' const DEFAULT_AUDIO_INPUT_DEVICE_ID = ''
const DEFAULT_AUDIO_INPUT_LANGUAGE = ''
const defaultSettings = { const defaultSettings = {
backendApiUrl: DEFAULT_BACKEND_API_URL, backendApiUrl: DEFAULT_BACKEND_API_URL,
@@ -38,6 +39,7 @@ const defaultSettings = {
openDevToolsOnStartup: DEFAULT_OPEN_DEVTOOLS_ON_STARTUP, openDevToolsOnStartup: DEFAULT_OPEN_DEVTOOLS_ON_STARTUP,
audioInputEnabled: DEFAULT_AUDIO_INPUT_ENABLED, audioInputEnabled: DEFAULT_AUDIO_INPUT_ENABLED,
audioInputDeviceId: DEFAULT_AUDIO_INPUT_DEVICE_ID, audioInputDeviceId: DEFAULT_AUDIO_INPUT_DEVICE_ID,
audioInputLanguage: DEFAULT_AUDIO_INPUT_LANGUAGE,
chatModel: 'llama3', chatModel: 'llama3',
} }
@@ -92,6 +94,7 @@ function migrateSettings(rawSettings) {
nextSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(nextSettings.openDevToolsOnStartup) nextSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(nextSettings.openDevToolsOnStartup)
nextSettings.audioInputEnabled = normalizeBooleanSetting(nextSettings.audioInputEnabled) nextSettings.audioInputEnabled = normalizeBooleanSetting(nextSettings.audioInputEnabled)
nextSettings.audioInputDeviceId = String(nextSettings.audioInputDeviceId || '').trim() nextSettings.audioInputDeviceId = String(nextSettings.audioInputDeviceId || '').trim()
nextSettings.audioInputLanguage = String(nextSettings.audioInputLanguage || '').trim().toLowerCase()
return { nextSettings, migrated } return { nextSettings, migrated }
} }
@@ -553,6 +556,8 @@ ipcMain.handle('set-setting', (event, key, value) => {
appSettings[key] = normalizeBooleanSetting(value) appSettings[key] = normalizeBooleanSetting(value)
} else if (key === 'audioInputDeviceId') { } else if (key === 'audioInputDeviceId') {
appSettings[key] = String(value || '').trim() appSettings[key] = String(value || '').trim()
} else if (key === 'audioInputLanguage') {
appSettings[key] = String(value || '').trim().toLowerCase()
} else { } else {
appSettings[key] = value appSettings[key] = value
} }
@@ -572,6 +577,7 @@ ipcMain.handle('update-settings', (event, settings) => {
appSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(appSettings.openDevToolsOnStartup) appSettings.openDevToolsOnStartup = normalizeOpenDevToolsOnStartup(appSettings.openDevToolsOnStartup)
appSettings.audioInputEnabled = normalizeBooleanSetting(appSettings.audioInputEnabled) appSettings.audioInputEnabled = normalizeBooleanSetting(appSettings.audioInputEnabled)
appSettings.audioInputDeviceId = String(appSettings.audioInputDeviceId || '').trim() appSettings.audioInputDeviceId = String(appSettings.audioInputDeviceId || '').trim()
appSettings.audioInputLanguage = String(appSettings.audioInputLanguage || '').trim().toLowerCase()
saveSettings() saveSettings()
if (Object.prototype.hasOwnProperty.call(settings, 'uiScale')) { if (Object.prototype.hasOwnProperty.call(settings, 'uiScale')) {
applyUiScaleToAllWindows() applyUiScaleToAllWindows()

View File

@@ -295,6 +295,7 @@ export default function App() {
const imageDragDepthRef = useRef(0) const imageDragDepthRef = useRef(0)
const [audioInputEnabled, setAudioInputEnabled] = useState(false) const [audioInputEnabled, setAudioInputEnabled] = useState(false)
const [audioInputDeviceId, setAudioInputDeviceId] = useState('') const [audioInputDeviceId, setAudioInputDeviceId] = useState('')
const [audioInputLanguage, setAudioInputLanguage] = useState('')
const [isRecordingAudio, setIsRecordingAudio] = useState(false) const [isRecordingAudio, setIsRecordingAudio] = useState(false)
const [isTranscribingAudio, setIsTranscribingAudio] = useState(false) const [isTranscribingAudio, setIsTranscribingAudio] = useState(false)
const [audioRecordingMs, setAudioRecordingMs] = useState(0) const [audioRecordingMs, setAudioRecordingMs] = useState(0)
@@ -554,6 +555,7 @@ export default function App() {
body: JSON.stringify({ body: JSON.stringify({
mime_type: mimeType || detectedMimeType || 'audio/webm', mime_type: mimeType || detectedMimeType || 'audio/webm',
audio_base64: payload, audio_base64: payload,
language: audioInputLanguage || null,
}), }),
}) })
const data = await expectBackendJson(response) const data = await expectBackendJson(response)
@@ -1144,6 +1146,7 @@ async function regenerateFromIndex(index, overrideUserText = null) {
setStreamOutput(settings.streamOutput || false); setStreamOutput(settings.streamOutput || false);
setAudioInputEnabled(settings.audioInputEnabled === true); setAudioInputEnabled(settings.audioInputEnabled === true);
setAudioInputDeviceId(typeof settings.audioInputDeviceId === 'string' ? settings.audioInputDeviceId : ''); setAudioInputDeviceId(typeof settings.audioInputDeviceId === 'string' ? settings.audioInputDeviceId : '');
setAudioInputLanguage(typeof settings.audioInputLanguage === 'string' ? settings.audioInputLanguage : '');
setScrollPositions(settings.scrollPositions || {}); // Load scroll positions setScrollPositions(settings.scrollPositions || {}); // Load scroll positions
applyColorScheme(settings.colorScheme || 'Default'); // Apply initial scheme applyColorScheme(settings.colorScheme || 'Default'); // Apply initial scheme
}).finally(() => { }).finally(() => {
@@ -2788,6 +2791,7 @@ async function createNewChat() {
onStreamOutputChange={setStreamOutput} onStreamOutputChange={setStreamOutput}
onAudioInputEnabledChange={setAudioInputEnabled} onAudioInputEnabledChange={setAudioInputEnabled}
onAudioInputDeviceChange={setAudioInputDeviceId} onAudioInputDeviceChange={setAudioInputDeviceId}
onAudioInputLanguageChange={setAudioInputLanguage}
onLibrariesPurged={handleLibrariesPurged} onLibrariesPurged={handleLibrariesPurged}
/> />
)} )}

View File

@@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
import { import {
AUDIO_INPUT_DEVICE_ID_KEY, AUDIO_INPUT_DEVICE_ID_KEY,
AUDIO_INPUT_ENABLED_KEY, AUDIO_INPUT_ENABLED_KEY,
AUDIO_INPUT_LANGUAGE_KEY,
AUDIO_INPUT_LANGUAGE_OPTIONS,
ensureAudioInputPermission, ensureAudioInputPermission,
listAudioInputDevices, listAudioInputDevices,
supportsAudioInputCapture, supportsAudioInputCapture,
@@ -13,6 +15,7 @@ const EMBED_MODEL_KEY = 'embedModel';
const MODEL_KEY = 'chatModel'; const MODEL_KEY = 'chatModel';
const STREAM_KEY = 'streamOutput'; const STREAM_KEY = 'streamOutput';
const DEFAULT_AUDIO_INPUT_DEVICE_ID = ''; const DEFAULT_AUDIO_INPUT_DEVICE_ID = '';
const DEFAULT_AUDIO_INPUT_LANGUAGE = '';
const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000'; const DEFAULT_BACKEND_API_URL = 'http://127.0.0.1:8000';
const DEFAULT_OLLAMA_API_URL = 'http://127.0.0.1:11434'; const DEFAULT_OLLAMA_API_URL = 'http://127.0.0.1:11434';
const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest'; const DEFAULT_EMBED_MODEL = 'nomic-embed-text:latest';
@@ -47,6 +50,7 @@ export default function GeneralSettings({
onBackendApiUrlChange, onBackendApiUrlChange,
onAudioInputEnabledChange, onAudioInputEnabledChange,
onAudioInputDeviceChange, onAudioInputDeviceChange,
onAudioInputLanguageChange,
}) { }) {
const [backendApiUrl, setBackendApiUrl] = useState(''); const [backendApiUrl, setBackendApiUrl] = useState('');
const [ollamaApiUrl, setOllamaApiUrl] = useState(''); const [ollamaApiUrl, setOllamaApiUrl] = useState('');
@@ -56,6 +60,7 @@ export default function GeneralSettings({
const [streamOutput, setStreamOutput] = useState(false); const [streamOutput, setStreamOutput] = useState(false);
const [audioInputEnabled, setAudioInputEnabled] = useState(false); const [audioInputEnabled, setAudioInputEnabled] = useState(false);
const [audioInputDeviceId, setAudioInputDeviceId] = useState(DEFAULT_AUDIO_INPUT_DEVICE_ID); const [audioInputDeviceId, setAudioInputDeviceId] = useState(DEFAULT_AUDIO_INPUT_DEVICE_ID);
const [audioInputLanguage, setAudioInputLanguage] = useState(DEFAULT_AUDIO_INPUT_LANGUAGE);
const [audioInputDevices, setAudioInputDevices] = useState([]); const [audioInputDevices, setAudioInputDevices] = useState([]);
const [isRefreshingAudioDevices, setIsRefreshingAudioDevices] = useState(false); const [isRefreshingAudioDevices, setIsRefreshingAudioDevices] = useState(false);
const [audioInputStatus, setAudioInputStatus] = useState({ tone: 'neutral', message: '' }); const [audioInputStatus, setAudioInputStatus] = useState({ tone: 'neutral', message: '' });
@@ -87,6 +92,11 @@ export default function GeneralSettings({
? settings.audioInputDeviceId ? settings.audioInputDeviceId
: DEFAULT_AUDIO_INPUT_DEVICE_ID : DEFAULT_AUDIO_INPUT_DEVICE_ID
); );
setAudioInputLanguage(
typeof settings.audioInputLanguage === 'string'
? settings.audioInputLanguage
: DEFAULT_AUDIO_INPUT_LANGUAGE
);
setUpdateStatus(status || DEFAULT_UPDATE_STATUS); setUpdateStatus(status || DEFAULT_UPDATE_STATUS);
}); });
@@ -268,6 +278,15 @@ export default function GeneralSettings({
} }
}; };
const handleAudioInputLanguageChange = (event) => {
const nextLanguage = event.target.value;
setAudioInputLanguage(nextLanguage);
window.electronAPI.setSetting(AUDIO_INPUT_LANGUAGE_KEY, nextLanguage);
if (onAudioInputLanguageChange) {
onAudioInputLanguageChange(nextLanguage);
}
};
const handleCheckForUpdates = async () => { const handleCheckForUpdates = async () => {
setIsCheckingForUpdates(true); setIsCheckingForUpdates(true);
try { try {
@@ -425,6 +444,18 @@ export default function GeneralSettings({
</option> </option>
))} ))}
</select> </select>
<select
className="select"
value={audioInputLanguage}
onChange={handleAudioInputLanguageChange}
disabled={!audioInputSupported}
>
{AUDIO_INPUT_LANGUAGE_OPTIONS.map(language => (
<option key={language.value || 'auto'} value={language.value}>
{language.label}
</option>
))}
</select>
<button <button
type="button" type="button"
className="button" className="button"
@@ -437,6 +468,9 @@ export default function GeneralSettings({
{audioInputStatus.message && ( {audioInputStatus.message && (
<p className={`setting-status ${audioInputStatus.tone}`}>{audioInputStatus.message}</p> <p className={`setting-status ${audioInputStatus.tone}`}>{audioInputStatus.message}</p>
)} )}
<p className="setting-description">
Whisper can auto-detect the spoken language, but you can force a fixed input language here when auto-detection drifts.
</p>
</> </>
)} )}
</div> </div>

View File

@@ -1,5 +1,22 @@
export const AUDIO_INPUT_ENABLED_KEY = 'audioInputEnabled' export const AUDIO_INPUT_ENABLED_KEY = 'audioInputEnabled'
export const AUDIO_INPUT_DEVICE_ID_KEY = 'audioInputDeviceId' export const AUDIO_INPUT_DEVICE_ID_KEY = 'audioInputDeviceId'
export const AUDIO_INPUT_LANGUAGE_KEY = 'audioInputLanguage'
export const AUDIO_INPUT_LANGUAGE_OPTIONS = [
{ value: '', label: 'Auto' },
{ value: 'de', label: 'German' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'French' },
{ value: 'es', label: 'Spanish' },
{ value: 'it', label: 'Italian' },
{ value: 'pt', label: 'Portuguese' },
{ value: 'nl', label: 'Dutch' },
{ value: 'pl', label: 'Polish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'ru', label: 'Russian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'zh', label: 'Chinese' },
]
const AUDIO_RECORDER_MIME_CANDIDATES = [ const AUDIO_RECORDER_MIME_CANDIDATES = [
'audio/webm;codecs=opus', 'audio/webm;codecs=opus',