Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/goose-sdk-types/src/custom_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,7 @@ pub struct DictationLocalModelStatus {
pub size_mb: u32,
pub downloaded: bool,
pub download_in_progress: bool,
pub recommended: bool,
}

/// Kick off a background download of a local Whisper model.
Expand Down
6 changes: 5 additions & 1 deletion crates/goose/acp-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5548,6 +5548,9 @@
},
"downloadInProgress": {
"type": "boolean"
},
"recommended": {
"type": "boolean"
}
},
"required": [
Expand All @@ -5556,7 +5559,8 @@
"description",
"sizeMb",
"downloaded",
"downloadInProgress"
"downloadInProgress",
"recommended"
]
},
"DictationModelDownloadRequest_unstable": {
Expand Down
2 changes: 2 additions & 0 deletions crates/goose/src/acp/server/dictation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ impl GooseAcpAgent {
use crate::download_manager::{get_download_manager, DownloadStatus};

let manager = get_download_manager();
let recommended_id = whisper::recommend_model();
let models = whisper::available_models()
.iter()
.map(|model| DictationLocalModelStatus {
Expand All @@ -172,6 +173,7 @@ impl GooseAcpAgent {
.get_progress(model.id)
.map(|progress| progress.status == DownloadStatus::Downloading)
.unwrap_or(false),
recommended: model.id == recommended_id,
})
.collect();

Expand Down
37 changes: 36 additions & 1 deletion ui/desktop/src/acp/dictation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { DictationProviderStatusEntry } from '@aaif/goose-sdk';
import type {
DictationDownloadProgress,
DictationLocalModelStatus,
DictationProviderStatusEntry,
} from '@aaif/goose-sdk';
import { getAcpClient } from './acpConnection';

export type { DictationProviderStatusEntry };

export type DictationProviders = Record<string, DictationProviderStatusEntry>;
export type LocalDictationModel = DictationLocalModelStatus;
export type LocalDictationDownloadProgress = DictationDownloadProgress;

export async function getDictationConfig(): Promise<DictationProviders> {
const client = await getAcpClient();
Expand All @@ -20,3 +26,32 @@ export async function transcribeDictation(
const response = await client.goose.dictationTranscribe_unstable({ audio, mimeType, provider });
return response.text;
}

export async function listLocalDictationModels(): Promise<LocalDictationModel[]> {
const client = await getAcpClient();
const response = await client.goose.dictationModelsList_unstable({});
return response.models;
}

export async function downloadLocalDictationModel(modelId: string): Promise<void> {
const client = await getAcpClient();
await client.goose.dictationModelsDownload_unstable({ modelId });
}

export async function getLocalDictationModelDownloadProgress(
modelId: string
): Promise<LocalDictationDownloadProgress | null> {
const client = await getAcpClient();
const response = await client.goose.dictationModelsDownloadProgress_unstable({ modelId });
return response.progress ?? null;
}

export async function cancelLocalDictationModelDownload(modelId: string): Promise<void> {
const client = await getAcpClient();
await client.goose.dictationModelsCancel_unstable({ modelId });
}

export async function deleteLocalDictationModel(modelId: string): Promise<void> {
const client = await getAcpClient();
await client.goose.dictationModelsDelete_unstable({ modelId });
}
48 changes: 23 additions & 25 deletions ui/desktop/src/components/settings/dictation/LocalModelManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { Download, Trash2, X, Check, ChevronDown, ChevronUp } from 'lucide-react
import { Button } from '../../ui/button';
import { useConfig } from '../../ConfigContext';
import {
listModels,
downloadModel,
getDownloadProgress,
cancelDownload as cancelDownloadApi,
deleteModel as deleteModelApi,
type WhisperModelResponse,
type DownloadProgress,
} from '../../../api';
cancelLocalDictationModelDownload,
deleteLocalDictationModel,
downloadLocalDictationModel,
getLocalDictationModelDownloadProgress,
listLocalDictationModels,
type LocalDictationDownloadProgress,
type LocalDictationModel,
} from '../../../acp/dictation';
import { defineMessages, useIntl } from '../../../i18n';

const i18n = defineMessages({
Expand Down Expand Up @@ -72,8 +72,10 @@ const capitalize = (str: string): string => {

export const LocalModelManager = () => {
const intl = useIntl();
const [models, setModels] = useState<WhisperModelResponse[]>([]);
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
const [models, setModels] = useState<LocalDictationModel[]>([]);
const [downloads, setDownloads] = useState<Map<string, LocalDictationDownloadProgress>>(
new Map()
);
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
const [showAllModels, setShowAllModels] = useState(false);
const { read, upsert } = useConfig();
Expand Down Expand Up @@ -105,18 +107,16 @@ export const LocalModelManager = () => {

const loadModels = async () => {
try {
const response = await listModels();
if (response.data) {
setModels(response.data);
}
const models = await listLocalDictationModels();
setModels(models);
} catch (error) {
console.error('Failed to load models:', error);
}
};

const startDownload = async (modelId: string) => {
try {
await downloadModel({ path: { model_id: modelId } });
await downloadLocalDictationModel(modelId);
pollDownloadProgress(modelId);
} catch (error) {
console.error('Failed to start download:', error);
Expand All @@ -126,9 +126,8 @@ export const LocalModelManager = () => {
const pollDownloadProgress = (modelId: string) => {
const interval = setInterval(async () => {
try {
const response = await getDownloadProgress({ path: { model_id: modelId } });
if (response.data) {
const progress = response.data;
const progress = await getLocalDictationModelDownloadProgress(modelId);
if (progress) {
setDownloads((prev) => new Map(prev).set(modelId, progress));

if (progress.status === 'completed') {
Expand All @@ -151,7 +150,7 @@ export const LocalModelManager = () => {

const cancelDownload = async (modelId: string) => {
try {
await cancelDownloadApi({ path: { model_id: modelId } });
await cancelLocalDictationModelDownload(modelId);
setDownloads((prev) => {
const next = new Map(prev);
next.delete(modelId);
Expand All @@ -167,7 +166,7 @@ export const LocalModelManager = () => {
if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return;

try {
await deleteModelApi({ path: { model_id: modelId } });
await deleteLocalDictationModel(modelId);
if (selectedModelId === modelId) {
await upsert(LOCAL_WHISPER_MODEL_CONFIG_KEY, '', false);
setSelectedModelId(null);
Expand Down Expand Up @@ -224,7 +223,7 @@ export const LocalModelManager = () => {
<h4 className="text-sm font-medium text-text-primary">
{capitalize(model.id)}
</h4>
<span className="text-xs text-text-secondary">{model.size_mb}MB</span>
<span className="text-xs text-text-secondary">{model.sizeMb}MB</span>
{model.recommended && (
<span className="text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
{intl.formatMessage(i18n.recommended)}
Expand Down Expand Up @@ -264,7 +263,7 @@ export const LocalModelManager = () => {
) : isDownloading ? (
<>
<div className="text-xs text-text-secondary min-w-[60px]">
{progress.progress_percent.toFixed(0)}%
{progress.progressPercent.toFixed(0)}%
</div>
<Button variant="ghost" size="sm" onClick={() => cancelDownload(model.id)}>
<X className="w-4 h-4" />
Expand All @@ -284,14 +283,13 @@ export const LocalModelManager = () => {
<div className="w-full bg-background-secondary rounded-full h-1.5">
<div
className="bg-background-inverse h-1.5 rounded-full transition-all"
style={{ width: `${progress.progress_percent}%` }}
style={{ width: `${progress.progressPercent}%` }}
/>
</div>
<div className="flex justify-between text-xs text-text-secondary">
<span>
{formatBytes(progress.bytes_downloaded)} / {formatBytes(progress.total_bytes)}
{formatBytes(progress.bytesDownloaded)} / {formatBytes(progress.totalBytes)}
</span>
{progress.speed_bps && <span>{formatBytes(progress.speed_bps)}/s</span>}
</div>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions ui/sdk/src/generated/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2256,6 +2256,7 @@ export type DictationLocalModelStatus = {
sizeMb: number;
downloaded: boolean;
downloadInProgress: boolean;
recommended: boolean;
};

/**
Expand Down
3 changes: 2 additions & 1 deletion ui/sdk/src/generated/zod.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2308,7 +2308,8 @@ export const zDictationLocalModelStatus = z.object({
description: z.string(),
sizeMb: z.number().int().gte(0),
downloaded: z.boolean(),
downloadInProgress: z.boolean()
downloadInProgress: z.boolean(),
recommended: z.boolean()
});

export const zDictationModelsListResponse_unstable = z.object({
Expand Down
Loading