mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-05 04:13:50 +00:00
Google does not publish gender for the 30 prebuilt Gemini voices —
inclusive design — but does publish a one-word style descriptor
(Bright, Firm, Smooth, Mature, Lively, etc.). Surface it so users can
pick by character.
- `audio.VoiceOption` gains `Labels map[string]string` (provider-
specific descriptors). ElevenLabs already returns labels via API;
Gemini now hardcodes `{"style": "..."}` per voice from the official
catalog.
- Web + desktop `mapCapVoiceToPortal` passes labels through. Both
pickers' `LABEL_KEYS` now include `"style"`, so the existing badge
rendering picks it up — first 1–2 entries shown next to the name.
- TS `VoiceOption` interface (web + desktop) gains optional `labels`.
No gender field is invented for Gemini voices.
138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { useHttp } from "@/hooks/use-ws";
|
|
|
|
// ---- Types ----
|
|
|
|
export interface EnumOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface Dependency {
|
|
field: string;
|
|
op: string;
|
|
value: unknown;
|
|
}
|
|
|
|
export type ParamType =
|
|
| "range"
|
|
| "number"
|
|
| "integer"
|
|
| "enum"
|
|
| "boolean"
|
|
| "string"
|
|
| "text";
|
|
|
|
export interface ParamSchema {
|
|
key: string;
|
|
type: ParamType;
|
|
label: string;
|
|
description?: string;
|
|
default?: unknown;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
enum?: EnumOption[];
|
|
depends_on?: Dependency[];
|
|
/** "advanced" → Advanced section; absent/undefined → Basic section (forward-compat: unknown values → Basic). */
|
|
group?: string;
|
|
/**
|
|
* When non-empty, this param is agent-overridable and the value is the
|
|
* generic key alias stored in agents.other_config.tts_params (e.g. "speed",
|
|
* "emotion", "style"). Empty / absent = not overridable.
|
|
* Used as the single source of truth for the generic↔native key mapping in
|
|
* the UI — no separate hard-coded lookup table needed (Finding #9).
|
|
*/
|
|
agent_overridable_as?: string;
|
|
}
|
|
|
|
export interface VoiceOption {
|
|
voice_id: string;
|
|
name: string;
|
|
language?: string;
|
|
gender?: string;
|
|
/** Provider-specific descriptors (e.g. {style: "Bright"} for Gemini). */
|
|
labels?: Record<string, string>;
|
|
}
|
|
|
|
export interface ProviderCapabilities {
|
|
provider: string;
|
|
display_name: string;
|
|
requires_api_key: boolean;
|
|
models?: string[];
|
|
voices?: VoiceOption[];
|
|
params?: ParamSchema[] | null;
|
|
custom_features?: Record<string, unknown> | null;
|
|
}
|
|
|
|
interface CapabilitiesResponse {
|
|
providers: ProviderCapabilities[];
|
|
}
|
|
|
|
// ---- parseCapabilitiesResponse ----
|
|
|
|
/**
|
|
* Parses and validates a raw GET /v1/tts/capabilities JSON response.
|
|
* Throws if the shape is invalid.
|
|
*/
|
|
export function parseCapabilitiesResponse(raw: unknown): ProviderCapabilities[] {
|
|
if (raw === null || raw === undefined || typeof raw !== "object") {
|
|
throw new Error("capabilities response must be an object");
|
|
}
|
|
const obj = raw as Record<string, unknown>;
|
|
if (!("providers" in obj)) {
|
|
throw new Error("capabilities response missing 'providers' field");
|
|
}
|
|
if (!Array.isArray(obj.providers)) {
|
|
throw new Error("capabilities response 'providers' must be an array");
|
|
}
|
|
return obj.providers as ProviderCapabilities[];
|
|
}
|
|
|
|
// ---- buildCapabilitiesQueryOptions ----
|
|
|
|
type CapabilitiesFetcher = () => Promise<CapabilitiesResponse>;
|
|
|
|
/**
|
|
* Builds React Query options for the TTS capabilities query.
|
|
* The queryFn catches errors and returns an empty providers array as fallback.
|
|
*/
|
|
export function buildCapabilitiesQueryOptions(fetcher: CapabilitiesFetcher) {
|
|
return {
|
|
queryKey: ["tts", "capabilities"] as const,
|
|
staleTime: 5 * 60_000, // 5 minutes — catalog data changes rarely
|
|
queryFn: async (_ctx: { signal: AbortSignal }): Promise<ProviderCapabilities[]> => {
|
|
try {
|
|
const resp = await fetcher();
|
|
return parseCapabilitiesResponse(resp);
|
|
} catch {
|
|
return [];
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---- useTtsCapabilities hook ----
|
|
|
|
export const ttsCapabilitiesKeys = {
|
|
all: ["tts", "capabilities"] as const,
|
|
};
|
|
|
|
/** React Query hook for fetching TTS provider capabilities. */
|
|
export function useTtsCapabilities() {
|
|
const http = useHttp();
|
|
return useQuery({
|
|
queryKey: ttsCapabilitiesKeys.all,
|
|
queryFn: async (): Promise<ProviderCapabilities[]> => {
|
|
try {
|
|
const resp = await http.get<CapabilitiesResponse>("/v1/tts/capabilities");
|
|
return parseCapabilitiesResponse(resp);
|
|
} catch {
|
|
return [];
|
|
}
|
|
},
|
|
staleTime: 5 * 60_000,
|
|
retry: 1,
|
|
});
|
|
}
|