Render ASR profile dialog from model descriptors

This commit is contained in:
rishikanthc
2026-05-08 11:27:59 -07:00
parent 8b0f863c05
commit 845f616475
3 changed files with 124 additions and 19 deletions

View File

@@ -2,7 +2,7 @@
Run ID: `ASR-PROFILE-FE`
Status: completed through ASR-PROFILE-FE-Sprint 2.
Status: completed through ASR-PROFILE-FE-Sprint 3.
This tracker belongs to `devnotes/v2.0.0/sprint-plans/asr-profile-provider-frontend-sprint-plan.md`.
@@ -201,32 +201,36 @@ Commit:
## ASR-PROFILE-FE-Sprint 3: Profile Dialog Revamp
Status: pending
Status: completed
Planned tasks:
- [ ] Keep profile name, description, and default toggle.
- [ ] Render a transcription step selector using transcription model cards.
- [ ] Save the first pipeline step as `kind`, `provider`, `model`, `model_family` if required, and `options`.
- [ ] Add an optional diarization step toggle.
- [ ] Render diarization parameters from a diarization model schema where available.
- [ ] Remove hard-coded language/task/thread/tail-padding/chunking/diarization controls from `ASRProfileDialog.tsx`.
- [ ] Add compact summaries for selected model, installed/download state, reload-required changes, and advanced fields.
- [x] Keep profile name, description, and default toggle.
- [x] Render a transcription step selector using transcription model cards.
- [x] Save the first pipeline step as `kind`, `provider`, `model`, `model_family` if required, and `options`.
- [x] Add an optional diarization step toggle.
- [x] Render diarization parameters from a diarization model schema where available.
- [x] Remove hard-coded language/task/thread/tail-padding/chunking/diarization controls from `ASRProfileDialog.tsx`.
- [x] Add compact summaries for selected model, installed/download state, reload-required changes, and advanced fields.
Acceptance checks:
- [ ] The dialog exposes every parameter in the selected transcription model schema.
- [ ] Parakeet TDT v2/v3 show Parakeet-supported parameters and no Whisper-only parameters.
- [ ] Whisper models show Whisper-specific language/task/timestamp controls.
- [ ] Diarization is descriptor-driven or explicitly blocked pending a backend model-card endpoint.
- [x] The dialog exposes every parameter in the selected transcription model schema.
- [x] Parakeet TDT v2/v3 show Parakeet-supported parameters and no Whisper-only parameters.
- [x] Whisper models show Whisper-specific language/task/timestamp controls.
- [x] Diarization is descriptor-driven or explicitly blocked pending a backend model-card endpoint.
Verification:
- [ ] Pending.
- [x] `npm --prefix web/frontend run build`
- [x] `npm --prefix web/frontend run lint` (passes with existing warnings outside this sprint)
- [x] `git diff --check -- web/frontend/src/features/settings/components/ASRProfileDialog.tsx web/frontend/src/features/settings/pages/SettingsPage.tsx devnotes/v2.0.0/sprint-trackers/asr-profile-provider-frontend-sprint-tracker.md`
Artifacts:
- Pending.
- `web/frontend/src/features/settings/components/ASRProfileDialog.tsx`
- `web/frontend/src/features/settings/pages/SettingsPage.tsx`
- `devnotes/v2.0.0/sprint-trackers/asr-profile-provider-frontend-sprint-tracker.md`
Commit:

View File

@@ -7,12 +7,16 @@ import {
type TranscriptionProfile,
type TranscriptionProfileOptions,
normalizeProfileOptions,
type ASRStep,
} from "../api/profilesApi";
import { ASRParameterForm } from "./ASRParameterForm";
import { resolveParameterValues, sanitizeParameterValues } from "./asrParameterValues";
type ASRProfileDialogProps = {
open: boolean;
profile: TranscriptionProfile | null;
models: TranscriptionModel[];
diarizationModels: TranscriptionModel[];
onClose: () => void;
onSave: (profile: {
id?: string;
@@ -30,7 +34,7 @@ const fallbackModels: TranscriptionModel[] = [
{ id: "parakeet-v3", display_name: "NVIDIA Parakeet TDT v3", provider: "local", installed: false, default: false, capabilities: { transcription: true, word_timestamps: true } },
];
export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASRProfileDialogProps) {
export function ASRProfileDialog({ open, profile, models, diarizationModels, onClose, onSave }: ASRProfileDialogProps) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [isDefault, setIsDefault] = useState(false);
@@ -38,6 +42,7 @@ export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASR
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const availableModels = models.length ? models : fallbackModels;
const availableDiarizationModels = diarizationModels;
const modelOptions = useMemo<SelectOption[]>(() => {
return availableModels.map((model) => ({
@@ -48,7 +53,11 @@ export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASR
}, [availableModels]);
const transcriptionStep = options.pipeline.find((step) => step.kind === "transcription");
const diarizationStep = options.pipeline.find((step) => step.kind === "diarization");
const selectedModelID = transcriptionStep?.model || availableModels.find((model) => model.default)?.id || availableModels[0]?.id || "";
const selectedModel = availableModels.find((model) => model.id === selectedModelID) || null;
const selectedDiarizationModelID = diarizationStep?.model || availableDiarizationModels.find((model) => model.default)?.id || availableDiarizationModels[0]?.id || "";
const selectedDiarizationModel = availableDiarizationModels.find((model) => model.id === selectedDiarizationModelID) || null;
useEffect(() => {
if (!open) return;
@@ -65,6 +74,27 @@ export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASR
setOptions((current) => withTranscriptionModel(current, availableModels, modelID));
};
const updateTranscriptionOptions = (values: Record<string, unknown>) => {
setOptions((current) => updateStepOptions(current, "transcription", values));
};
const updateDiarizationModel = (modelID: string) => {
setOptions((current) => withDiarizationModel(current, availableDiarizationModels, modelID));
};
const updateDiarizationOptions = (values: Record<string, unknown>) => {
setOptions((current) => updateStepOptions(current, "diarization", values));
};
const toggleDiarization = (enabled: boolean) => {
setOptions((current) => {
if (!enabled) {
return { pipeline: current.pipeline.filter((step) => step.kind !== "diarization") };
}
return withDiarizationModel(current, availableDiarizationModels, selectedDiarizationModelID);
});
};
const submit = async () => {
const cleanName = name.trim();
if (!cleanName) {
@@ -79,7 +109,7 @@ export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASR
name: cleanName,
description: description.trim(),
is_default: isDefault,
options: ensureTranscriptionStep(options, availableModels),
options: prepareProfileOptionsForSave(options, availableModels, availableDiarizationModels),
});
onClose();
} catch (err) {
@@ -120,6 +150,31 @@ export function ASRProfileDialog({ open, profile, models, onClose, onSave }: ASR
<SelectField label="Model" value={selectedModelID} options={modelOptions} onChange={updateModel} />
</div>
</section>
<ASRParameterForm
model={selectedModel}
values={transcriptionStep?.options || {}}
onChange={updateTranscriptionOptions}
/>
<section className="scr-settings-section">
<h3 className="scr-settings-section-title">Diarization</h3>
<CheckRow label="Identify speakers" checked={Boolean(diarizationStep)} onChange={toggleDiarization} />
{diarizationStep && availableDiarizationModels.length > 0 ? (
<div className="scr-form-grid">
<SelectField label="Model" value={selectedDiarizationModelID} options={diarizationModelOptions(availableDiarizationModels)} onChange={updateDiarizationModel} />
</div>
) : null}
{diarizationStep && availableDiarizationModels.length === 0 ? <div className="scr-alert">No diarization model card is available.</div> : null}
</section>
{diarizationStep ? (
<ASRParameterForm
model={selectedDiarizationModel}
values={diarizationStep.options || {}}
onChange={updateDiarizationOptions}
/>
) : null}
</div>
<footer className="scr-modal-footer">
@@ -166,12 +221,56 @@ function ensureTranscriptionStep(options: TranscriptionProfileOptions, models: T
function withTranscriptionModel(options: TranscriptionProfileOptions, models: TranscriptionModel[], modelID: string): TranscriptionProfileOptions {
const model = models.find((item) => item.id === modelID) || fallbackModels.find((item) => item.id === modelID) || fallbackModels[0];
const existingStep = options.pipeline.find((step) => step.kind === "transcription");
const nextStep = {
...(options.pipeline.find((step) => step.kind === "transcription") || {}),
...(existingStep || {}),
kind: "transcription" as const,
provider: model.provider,
model: model.id,
options: sanitizeParameterValues(model, resolveParameterValues(model, existingStep?.options || {})),
};
const otherSteps = options.pipeline.filter((step) => step.kind !== "transcription");
return { pipeline: [nextStep, ...otherSteps] };
}
function withDiarizationModel(options: TranscriptionProfileOptions, models: TranscriptionModel[], modelID: string): TranscriptionProfileOptions {
const model = models.find((item) => item.id === modelID) || models[0];
if (!model) return options;
const existingStep = options.pipeline.find((step) => step.kind === "diarization");
const nextStep = {
...(existingStep || {}),
kind: "diarization" as const,
provider: model.provider,
model: model.id,
options: sanitizeParameterValues(model, resolveParameterValues(model, existingStep?.options || {})),
};
const otherSteps = options.pipeline.filter((step) => step.kind !== "diarization");
return { pipeline: [...otherSteps, nextStep] };
}
function updateStepOptions(options: TranscriptionProfileOptions, kind: ASRStep["kind"], values: Record<string, unknown>): TranscriptionProfileOptions {
return {
pipeline: options.pipeline.map((step) => (step.kind === kind ? { ...step, options: values } : step)),
};
}
function prepareProfileOptionsForSave(options: TranscriptionProfileOptions, models: TranscriptionModel[], diarizationModels: TranscriptionModel[]): TranscriptionProfileOptions {
const withTranscription = ensureTranscriptionStep(options, models);
return {
pipeline: withTranscription.pipeline.map((step) => {
const model = step.kind === "diarization"
? diarizationModels.find((item) => item.id === step.model)
: models.find((item) => item.id === step.model) || fallbackModels.find((item) => item.id === step.model);
if (!model) return step;
return { ...step, options: sanitizeParameterValues(model, resolveParameterValues(model, step.options || {})) };
}),
};
}
function diarizationModelOptions(models: TranscriptionModel[]): SelectOption[] {
return models.map((model) => ({
value: model.id,
label: model.display_name || model.id,
description: model.installed ? "Installed locally" : "Downloads on use",
}));
}

View File

@@ -4,7 +4,7 @@ import { Sidebar } from "@/features/home/components/HomePage";
import { AppButton, IconButton } from "@/shared/ui/Button";
import { ConfirmDialog } from "@/shared/ui/ConfirmDialog";
import { EmptyState } from "@/shared/ui/EmptyState";
import { useDeleteProfile, useProfiles, useSaveProfile, useTranscriptionModels } from "@/features/settings/hooks/useProfiles";
import { useASRModels, useDeleteProfile, useProfiles, useSaveProfile, useTranscriptionModels } from "@/features/settings/hooks/useProfiles";
import { ASRProfileDialog } from "../components/ASRProfileDialog";
import { GeneralSettingsPanel } from "../components/GeneralSettingsPanel";
import { LLMProviderPanel } from "../components/LLMProviderPanel";
@@ -22,6 +22,7 @@ export function Settings() {
const [profileToDelete, setProfileToDelete] = useState<TranscriptionProfile | null>(null);
const profilesQuery = useProfiles();
const modelsQuery = useTranscriptionModels();
const diarizationModelsQuery = useASRModels(["diarization"]);
const saveProfileMutation = useSaveProfile();
const deleteProfileMutation = useDeleteProfile();
const profiles = useMemo(() => profilesQuery.data ?? [], [profilesQuery.data]);
@@ -142,6 +143,7 @@ export function Settings() {
open={dialogOpen}
profile={editingProfile}
models={modelsQuery.data || []}
diarizationModels={diarizationModelsQuery.data || []}
onClose={() => {
setDialogOpen(false);
setEditingProfile(null);