feat(transcription): add shared voice input support (#4232)
* feat(webui): add voice transcription input * feat(webui): render ANSI output in code blocks * refactor(webui): isolate voice recorder logic * refactor(transcription): keep websocket ingress thin * refactor(transcription): resolve channel audio settings on demand * style(webui): neutralize voice waveform color * feat(webui): add voice input tooltip * feat(webui): add voice input keyboard shortcut * fix(webui): distinguish voice shortcut platforms * fix(webui): place voice button after model selector * refactor(webui): share voice hold recording helpers * fix(desktop): allow microphone voice input * fix(webui): stabilize token usage month labels * feat(webui): show voice input on settings overview * fix(webui): label voice capability as recognition * fix(webui): align capability overview status * refactor(webui): isolate transcription socket handling * fix(webui): soften silent voice waveform * refactor(audio): clarify transcription service location * docs(transcription): clarify audio and provider boundaries * fix(exec): reduce session output polling flake
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
protocol,
|
||||
session,
|
||||
shell,
|
||||
systemPreferences,
|
||||
} from "electron";
|
||||
import type { IpcMainInvokeEvent, WebContents } from "electron";
|
||||
|
||||
@@ -100,6 +101,58 @@ function isTrustedAppUrl(rawUrl: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function isTrustedPermissionRequest(
|
||||
webContents: WebContents | null,
|
||||
details: unknown,
|
||||
): boolean {
|
||||
return [
|
||||
permissionDetail(details, "requestingUrl"),
|
||||
permissionDetail(details, "securityOrigin"),
|
||||
webContents?.getURL(),
|
||||
].some((url) => typeof url === "string" && isTrustedAppUrl(url));
|
||||
}
|
||||
|
||||
function permissionDetail(details: unknown, key: string): unknown {
|
||||
return typeof details === "object" && details !== null
|
||||
? (details as Record<string, unknown>)[key]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isAudioOnlyMediaRequest(details: unknown): boolean {
|
||||
const mediaTypes = permissionDetail(details, "mediaTypes");
|
||||
if (Array.isArray(mediaTypes)) {
|
||||
return mediaTypes.includes("audio") && !mediaTypes.includes("video");
|
||||
}
|
||||
return permissionDetail(details, "mediaType") === "audio";
|
||||
}
|
||||
|
||||
async function requestNativeMicrophoneAccess(): Promise<boolean> {
|
||||
if (process.platform !== "darwin") return true;
|
||||
const status = systemPreferences.getMediaAccessStatus("microphone");
|
||||
if (status === "granted") return true;
|
||||
if (status === "denied" || status === "restricted") return false;
|
||||
return await systemPreferences.askForMediaAccess("microphone");
|
||||
}
|
||||
|
||||
function registerPermissionHandlers(): void {
|
||||
session.defaultSession.setPermissionCheckHandler((webContents, permission, _origin, details) => (
|
||||
permission === "media"
|
||||
&& isTrustedPermissionRequest(webContents, details)
|
||||
&& isAudioOnlyMediaRequest(details)
|
||||
));
|
||||
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
||||
if (
|
||||
permission !== "media"
|
||||
|| !isTrustedPermissionRequest(webContents, details)
|
||||
|| !isAudioOnlyMediaRequest(details)
|
||||
) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
void requestNativeMicrophoneAccess().then(callback, () => callback(false));
|
||||
});
|
||||
}
|
||||
|
||||
function assertTrustedIpc(event: IpcMainInvokeEvent): void {
|
||||
const frameUrl = event.senderFrame?.url || event.sender.getURL();
|
||||
if (!isTrustedAppUrl(frameUrl)) {
|
||||
@@ -749,6 +802,7 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
|
||||
registerIpcHandlers();
|
||||
registerPermissionHandlers();
|
||||
registerAppProtocol(webDist, devUrl);
|
||||
|
||||
mainWindow = createWindow();
|
||||
|
||||
Reference in New Issue
Block a user