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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
/electron-dist/
/dist/

# Locally-served speech models (tools/crisperwhisper-onnx/install_local.py).
# Hundreds of MB of ONNX weights, reproducible from the export tooling, and
# non-commercially licensed — they must not be committed.
/public/models/

# misc
.DS_Store
*.pem
Expand Down
2 changes: 1 addition & 1 deletion components/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ export default function ExportDialog() {
)}

{error && (
<p className="mb-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600 dark:bg-red-950/40 dark:text-red-400">
<p className="mb-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900/30 dark:bg-red-950/30 dark:text-red-900">
{error}
</p>
)}
Expand Down
54 changes: 38 additions & 16 deletions components/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useId,
useMemo,
useState,
type ComponentType,
type ReactNode,
} from "react";
import {
Expand All @@ -18,8 +19,12 @@ import {
FileText,
Languages,
Loader2,
type LucideIcon,
} from "lucide-react";
import {
SignalBarsHigh,
SignalBarsLow,
SignalBarsMedium,
} from "./SignalBars";
import {
TRANSCRIPT_LANGUAGE_ORDER,
TRANSCRIPT_LANGUAGES,
Expand Down Expand Up @@ -50,9 +55,15 @@ export type ModelOptionContextValue = {
keepMenuOpen: () => void;
};

/** Any `size` + `className` icon: lucide's, or a local one like {@link SignalBarsLow}. */
export type IconComponent = ComponentType<{
size?: number | string;
className?: string;
}>;

export type OptionTrigger = {
label: ReactNode;
icon?: LucideIcon;
icon?: IconComponent;
iconClassName?: string;
/** Show a spinner instead of the icon in the closed trigger. */
busy?: boolean;
Expand Down Expand Up @@ -301,12 +312,26 @@ export default function ModelSelector({
);
}

/**
* Signal bars stand in for relative model strength on the default rows.
*
* A plain lookup rather than a `iconForSource(id)` helper: the row renders this
* value as JSX, and react-hooks/static-components reads any call result used as
* a component type as a component created during render.
*/
const SOURCE_ICONS: Record<TranscriptSource, IconComponent> = {
base: SignalBarsLow,
small: SignalBarsMedium,
parakeet: SignalBarsHigh,
import: FileText,
};

/** Default option row: icon + label + optional meta. ASR ids fill in from MODELS. */
export function ModelOption({
id,
label,
meta,
icon: Icon = AudioLines,
icon,
children,
onSelect,
/** When false, a child owns the closed trigger via `useOptionTrigger`. */
Expand All @@ -315,14 +340,15 @@ export function ModelOption({
id: TranscriptSource;
label?: string;
meta?: string;
icon?: LucideIcon;
icon?: IconComponent;
children?: ReactNode;
onSelect?: (ctx: ModelOptionContextValue) => void;
autoTrigger?: boolean;
}) {
const selector = useSelectorCtx();
const selected = selector.value === id;

const Icon = icon ?? SOURCE_ICONS[id];
const resolvedLabel = label ?? (isModelId(id) ? MODELS[id].label : id);
const resolvedMeta = meta ?? (isModelId(id) ? MODELS[id].size : undefined);

Expand Down Expand Up @@ -355,11 +381,10 @@ export function ModelOption({
role="option"
aria-selected={selected}
onClick={handleClick}
className={`flex w-full flex-col gap-0.5 rounded-lg px-2.5 py-2 text-left transition cursor-pointer ${
selected
className={`flex w-full flex-col gap-0.5 rounded-lg px-2.5 py-2 text-left transition cursor-pointer ${selected
? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
: "text-zinc-700 hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-800/60"
}`}
}`}
>
<span className="flex w-full items-center gap-2.5">
<Icon
Expand Down Expand Up @@ -425,11 +450,10 @@ export function LanguageSection() {
setSubmenuOpen((v) => !v);
selector.keepMenuOpen();
}}
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition ${
submenuOpen
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition ${submenuOpen
? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
: "text-zinc-700 hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-800/60"
}`}
}`}
>
<span className="text-[15px] leading-none" aria-hidden>
{active.flag}
Expand All @@ -439,9 +463,8 @@ export function LanguageSection() {
</span>
<ChevronRight
size={14}
className={`shrink-0 text-zinc-400 transition dark:text-zinc-500 ${
submenuOpen ? "rotate-180" : ""
}`}
className={`shrink-0 text-zinc-400 transition dark:text-zinc-500 ${submenuOpen ? "rotate-180" : ""
}`}
/>
</button>
</PopoverTrigger>
Expand All @@ -462,11 +485,10 @@ export function LanguageSection() {
role="menuitemradio"
aria-checked={selected}
onClick={() => select(id)}
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition ${
selected
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition ${selected
? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
: "text-zinc-700 hover:bg-zinc-50 dark:text-zinc-300 dark:hover:bg-zinc-800/60"
}`}
}`}
>
<span className="text-[15px] leading-none" aria-hidden>
{option.flag}
Expand Down
67 changes: 67 additions & 0 deletions components/SignalBars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Signal-strength bars for the model rows, drawn here rather than taken from
* lucide: `SignalLow` and friends omit the bars above the level, so the three
* model rows would each get a differently-shaped, differently-sized glyph.
* These always draw all three bars and fade the inactive ones, so the rows share
* one silhouette and read as "level N of 3".
*/

export type SignalBarsProps = {
size?: number | string;
className?: string;
};

/** x position and top y of each bar, shortest first. Bars sit on y = 19. */
const BARS: ReadonlyArray<{ x: number; top: number }> = [
{ x: 5.75, top: 14.5 },
{ x: 12, top: 10.25 },
{ x: 18.25, top: 6 },
];

const BASELINE = 19;
/** Matches the 2px stroke of the lucide icons beside it, plus round caps. */
const STROKE = 2.5;
const INACTIVE_OPACITY = 0.25;

function SignalBars({
level,
size = 24,
className,
}: SignalBarsProps & { level: 1 | 2 | 3 }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={STROKE}
strokeLinecap="round"
className={className}
aria-hidden
>
{BARS.map((bar, i) => (
<line
key={bar.x}
x1={bar.x}
y1={bar.top}
x2={bar.x}
y2={BASELINE}
opacity={i < level ? 1 : INACTIVE_OPACITY}
/>
))}
</svg>
);
}

export function SignalBarsLow(props: SignalBarsProps) {
return <SignalBars level={1} {...props} />;
}

export function SignalBarsMedium(props: SignalBarsProps) {
return <SignalBars level={2} {...props} />;
}

export function SignalBarsHigh(props: SignalBarsProps) {
return <SignalBars level={3} {...props} />;
}
4 changes: 2 additions & 2 deletions components/SpeakerLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export default function SpeakerLabel({
? "Find a speaker…"
: "Search or create…"
}
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-2 py-1.5 text-[13px] text-zinc-800 outline-none focus:border-zinc-400 focus:bg-white dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:focus:border-zinc-500"
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-2 py-1.5 text-[13px] text-zinc-800 outline-none focus:border-zinc-400 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:focus:border-zinc-500"
/>
</div>

Expand Down Expand Up @@ -481,7 +481,7 @@ export function SelectionSpeakerPopover({
else if (canCreate) apply("new", trimmedQuery);
}}
placeholder="Search or create…"
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-2 py-1.5 text-[13px] text-zinc-800 outline-none focus:border-zinc-400 focus:bg-white dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:border-zinc-400"
className="w-full rounded-lg border border-zinc-200 bg-zinc-50 px-2 py-1.5 text-[13px] text-zinc-800 outline-none focus:border-zinc-400 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:bg-zinc-900 dark:focus:border-zinc-400"
/>
</div>
<div className="max-h-56 overflow-y-auto py-1">
Expand Down
2 changes: 1 addition & 1 deletion components/TranscriptPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export default function TranscriptPanel() {
)}

{status === "error" && (
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-600 dark:border-red-900 dark:bg-red-950/40 dark:text-red-400">
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-600 dark:border-red-900/30 dark:bg-red-950/30 dark:text-red-900">
{error}
</div>
)}
Expand Down
Loading
Loading