Update dependencies and enhance UI components for better functionality

- Added new Tauri plugins: @tauri-apps/plugin-process and @tauri-apps/plugin-updater to improve application capabilities.
- Introduced UpdateStatus component to display update information in the UI.
- Enhanced GenerationForm to include an optional instruct field for additional input.
- Refactored various components for improved styling and responsiveness, including Sidebar, AudioPlayer, and ProfileCard.
- Updated API models and schemas to accommodate new instruct parameter in generation requests and responses.
- Improved documentation for autoupdater setup and usage.
This commit is contained in:
Jamie Pine
2026-01-25 16:38:25 -08:00
parent 45c4c2f488
commit e7e3a0a440
87 changed files with 2421 additions and 1628 deletions
+2
View File
@@ -31,6 +31,8 @@
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-devtools": "^5.0.0",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
+6 -1
View File
@@ -3,11 +3,13 @@ import { GenerationForm } from '@/components/Generation/GenerationForm';
import { HistoryTable } from '@/components/History/HistoryTable';
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
import { Toaster } from '@/components/ui/toaster';
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
import { Sidebar } from '@/components/Sidebar';
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
import { UpdateNotification } from '@/components/UpdateNotification';
import { isTauri, startServer, setupWindowCloseHandler } from '@/lib/tauri';
// Track if server is starting to prevent duplicate starts
@@ -90,12 +92,15 @@ function App() {
<main className="flex-1 ml-20 overflow-hidden flex flex-col">
<div className="container mx-auto px-8 py-8 max-w-[1800px] h-full overflow-hidden flex flex-col">
<UpdateNotification />
{activeTab === 'settings' ? (
<div className="space-y-4 overflow-y-auto">
<div className="grid gap-4 md:grid-cols-2">
<ConnectionForm />
<ServerStatus />
</div>
{isTauri() && <UpdateStatus />}
<ModelManagement />
</div>
) : (
@@ -107,7 +112,7 @@ function App() {
<div className="shrink-0 flex flex-col">
<ProfileList />
</div>
{/* Generator - Bottom Left */}
<div className="shrink-0">
<GenerationForm />
@@ -34,12 +34,12 @@ export function AudioPlayer() {
if (!audioUrl) {
return;
}
if (wavesurferRef.current) {
console.log('WaveSurfer already initialized, skipping');
return;
}
console.log('Creating NEW WaveSurfer instance');
// Wait for container to be properly rendered
@@ -241,7 +241,7 @@ export function AudioPlayer() {
// Load audio when URL changes (only if WaveSurfer is already initialized)
useEffect(() => {
const wavesurfer = wavesurferRef.current;
if (!audioUrl || !wavesurfer) {
// Reset state when no audio or WaveSurfer not ready
if (!audioUrl && wavesurfer) {
@@ -259,7 +259,7 @@ export function AudioPlayer() {
// CRITICAL: Force stop any current playback and cancel any pending loads
// This must happen BEFORE any early returns
console.log('Audio URL changed to:', audioUrl);
// COMPLETELY stop and destroy the current audio
try {
// First pause if playing
@@ -267,7 +267,7 @@ export function AudioPlayer() {
console.log('Pausing current playback');
wavesurfer.pause();
}
// Stop the media element explicitly
const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) {
@@ -276,7 +276,7 @@ export function AudioPlayer() {
mediaElement.currentTime = 0;
mediaElement.src = '';
}
// Use empty() to completely destroy the waveform and media element
console.log('Calling wavesurfer.empty() to destroy audio');
wavesurfer.empty();
@@ -284,7 +284,7 @@ export function AudioPlayer() {
console.error('Error stopping previous audio:', error);
// Continue anyway to load new audio
}
// Reset loading state to allow new load (cancel any pending loads)
loadingRef.current = false;
@@ -33,6 +33,7 @@ const generationSchema = z.object({
language: z.enum(['en', 'zh']),
seed: z.number().int().optional(),
modelSize: z.enum(['1.7B', '0.6B']).optional(),
instruct: z.string().max(500).optional(),
});
type GenerationFormValues = z.infer<typeof generationSchema>;
@@ -50,6 +51,7 @@ export function GenerationForm() {
language: 'en',
seed: undefined,
modelSize: '1.7B',
instruct: '',
},
});
@@ -70,6 +72,7 @@ export function GenerationForm() {
language: data.language,
seed: data.seed,
model_size: data.modelSize,
instruct: data.instruct || undefined,
});
toast({
@@ -119,7 +122,7 @@ export function GenerationForm() {
<FormControl>
<Textarea
placeholder="Enter the text you want to generate..."
className="min-h-[200px]"
className="min-h-[150px]"
{...field}
/>
</FormControl>
@@ -129,6 +132,28 @@ export function GenerationForm() {
)}
/>
<FormField
control={form.control}
name="instruct"
render={({ field }) => (
<FormItem>
<FormLabel>Delivery Instructions (optional)</FormLabel>
<FormControl>
<Textarea
placeholder="e.g. Speak slowly with emphasis, Warm and friendly tone, Professional and authoritative..."
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormDescription>
Natural language instructions to control speech delivery (tone, emotion, pace).
Max 500 characters
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4 md:grid-cols-3">
<FormField
control={form.control}
@@ -77,9 +77,7 @@ export function ConnectionForm() {
)}
/>
{isDirty && (
<Button type="submit">Update Connection</Button>
)}
{isDirty && <Button type="submit">Update Connection</Button>}
</form>
</Form>
@@ -106,8 +104,8 @@ export function ConnectionForm() {
Keep server running when app closes
</label>
<p className="text-sm text-muted-foreground">
When enabled, the server will continue running in the background after closing the app.
Disabled by default.
When enabled, the server will continue running in the background after closing the
app. Disabled by default.
</p>
</div>
</div>
@@ -73,7 +73,9 @@ export function ModelManagement() {
<div className="space-y-4">
{/* TTS Models */}
<div>
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">Voice Generation Models</h3>
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">
Voice Generation Models
</h3>
<div className="space-y-2">
{modelStatus.models
.filter((m) => m.model_name.startsWith('qwen-tts'))
@@ -91,7 +93,9 @@ export function ModelManagement() {
{/* Whisper Models */}
<div>
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">Transcription Models</h3>
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">
Transcription Models
</h3>
<div className="space-y-2">
{modelStatus.models
.filter((m) => m.model_name.startsWith('whisper'))
@@ -109,7 +113,9 @@ export function ModelManagement() {
{/* Progress indicators */}
<div className="pt-4 border-t">
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">Download Progress</h3>
<h3 className="text-sm font-semibold mb-3 text-muted-foreground">
Download Progress
</h3>
<div className="space-y-2">
{modelStatus.models.map((model) => (
<ModelProgress
@@ -147,10 +153,14 @@ function ModelItem({ model, onDownload, isDownloading, formatSize }: ModelItemPr
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{model.display_name}</span>
{model.loaded && (
<Badge variant="default" className="text-xs">Loaded</Badge>
<Badge variant="default" className="text-xs">
Loaded
</Badge>
)}
{model.downloaded && !model.loaded && (
<Badge variant="secondary" className="text-xs">Downloaded</Badge>
<Badge variant="secondary" className="text-xs">
Downloaded
</Badge>
)}
</div>
{model.downloaded && model.size_mb && (
@@ -166,12 +176,7 @@ function ModelItem({ model, onDownload, isDownloading, formatSize }: ModelItemPr
<span>Ready</span>
</div>
) : (
<Button
size="sm"
onClick={onDownload}
disabled={isDownloading}
variant="outline"
>
<Button size="sm" onClick={onDownload} disabled={isDownloading} variant="outline">
{isDownloading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
@@ -25,7 +25,7 @@ export function ModelProgress({ modelName, displayName }: ModelProgressProps) {
try {
const data = JSON.parse(event.data) as ModelProgressType;
setProgress(data);
// Close connection if complete or error
if (data.status === 'complete' || data.status === 'error') {
eventSource.close();
@@ -51,7 +51,10 @@ export function ModelProgress({ modelName, displayName }: ModelProgressProps) {
}, [serverUrl, modelName, isSubscribed]);
// Don't render if no progress or if complete/error and some time has passed
if (!progress || (progress.status === 'complete' && Date.now() - new Date(progress.timestamp).getTime() > 5000)) {
if (
!progress ||
(progress.status === 'complete' && Date.now() - new Date(progress.timestamp).getTime() > 5000)
) {
return null;
}
@@ -111,9 +114,7 @@ export function ModelProgress({ modelName, displayName }: ModelProgressProps) {
</span>
)}
</div>
{progress.total > 0 && (
<Progress value={progress.progress} className="h-2" />
)}
{progress.total > 0 && <Progress value={progress.progress} className="h-2" />}
</div>
</CardContent>
</Card>
@@ -47,7 +47,9 @@ export function ServerStatus() {
<span className="text-sm">Connected</span>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant={health.model_loaded || health.model_downloaded ? 'default' : 'secondary'}>
<Badge
variant={health.model_loaded || health.model_downloaded ? 'default' : 'secondary'}
>
{health.model_loaded || health.model_downloaded ? 'Model Ready' : 'No Model'}
</Badge>
<Badge variant={health.gpu_available ? 'default' : 'secondary'}>
@@ -0,0 +1,101 @@
import { useState, useEffect } from 'react';
import { RefreshCw, Download, CheckCircle2, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { useAutoUpdater } from '@/hooks/useAutoUpdater';
import { getVersion } from '@tauri-apps/api/app';
export function UpdateStatus() {
const { status, checkForUpdates, downloadAndInstall } = useAutoUpdater(false);
const [currentVersion, setCurrentVersion] = useState<string>('');
useEffect(() => {
getVersion()
.then(setCurrentVersion)
.catch(() => setCurrentVersion('0.1.0'));
}, []);
return (
<Card>
<CardHeader>
<CardTitle>App Updates</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="text-sm font-medium">Current Version</div>
<div className="text-sm text-muted-foreground">v{currentVersion}</div>
</div>
<Button
onClick={checkForUpdates}
disabled={status.checking || status.downloading || status.installing}
variant="outline"
size="sm"
>
<RefreshCw className={`h-4 w-4 mr-2 ${status.checking ? 'animate-spin' : ''}`} />
Check for Updates
</Button>
</div>
{status.checking && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<RefreshCw className="h-4 w-4 animate-spin" />
Checking for updates...
</div>
)}
{status.error && (
<div className="flex items-center gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4" />
{status.error}
</div>
)}
{status.available && !status.downloading && !status.installing && (
<div className="space-y-3 p-4 border rounded-lg bg-primary/5">
<div className="flex items-center justify-between">
<div>
<div className="font-semibold">Update Available</div>
<div className="text-sm text-muted-foreground">Version {status.version}</div>
</div>
<Badge>New</Badge>
</div>
<Button onClick={downloadAndInstall} className="w-full" size="sm">
<Download className="h-4 w-4 mr-2" />
Install Update
</Button>
</div>
)}
{status.downloading && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Download className="h-4 w-4" />
Downloading update...
</div>
<Progress />
</div>
)}
{status.installing && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<RefreshCw className="h-4 w-4 animate-spin" />
Installing update...
</div>
<div className="text-xs text-muted-foreground">App will restart automatically</div>
</div>
)}
{!status.available && !status.checking && !status.error && status.checking === false && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="h-4 w-4 text-green-500" />
You're up to date
</div>
)}
</CardContent>
</Card>
);
}
+4 -6
View File
@@ -19,18 +19,16 @@ export function Sidebar({ activeTab, onTabChange }: SidebarProps) {
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
type="button"
onClick={() => onTabChange(tab.id)}
className={cn(
"w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200",
"hover:bg-accent hover:text-accent-foreground",
isActive
? "bg-primary text-primary-foreground shadow-lg"
: "text-muted-foreground"
'w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200',
'hover:bg-accent hover:text-accent-foreground',
isActive ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground',
)}
title={tab.label}
aria-label={tab.label}
@@ -57,8 +57,8 @@ export function ProfileCard({ profile }: ProfileCardProps) {
<>
<Card
className={cn(
"cursor-pointer hover:shadow-md transition-all",
isSelected && "ring-2 ring-primary shadow-md"
'cursor-pointer hover:shadow-md transition-all',
isSelected && 'ring-2 ring-primary shadow-md',
)}
onClick={handleSelect}
>
@@ -102,7 +102,9 @@ export function ProfileCard({ profile }: ProfileCardProps) {
<Badge variant="outline" className="text-xs h-5 px-1.5">
{profile.language}
</Badge>
<p className="text-xs text-muted-foreground/60 text-right">{formatDate(profile.created_at)}</p>
<p className="text-xs text-muted-foreground/60 text-right">
{formatDate(profile.created_at)}
</p>
</div>
</CardContent>
</Card>
@@ -121,7 +123,11 @@ export function ProfileCard({ profile }: ProfileCardProps) {
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDeleteConfirm} disabled={deleteProfile.isPending}>
<Button
variant="destructive"
onClick={handleDeleteConfirm}
disabled={deleteProfile.isPending}
>
{deleteProfile.isPending ? 'Deleting...' : 'Delete'}
</Button>
</DialogFooter>
+525 -65
View File
@@ -1,5 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
@@ -13,6 +13,7 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -27,15 +28,64 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useToast } from '@/components/ui/use-toast';
import { useCreateProfile, useProfile, useUpdateProfile } from '@/lib/hooks/useProfiles';
import {
useCreateProfile,
useProfile,
useUpdateProfile,
useAddSample,
} from '@/lib/hooks/useProfiles';
import { useTranscription } from '@/lib/hooks/useTranscription';
import { useAudioRecording } from '@/lib/hooks/useAudioRecording';
import { useUIStore } from '@/stores/uiStore';
import { Mic, Square, Upload } from 'lucide-react';
import { formatAudioDuration } from '@/lib/utils/audio';
const profileSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
description: z.string().max(500).optional(),
language: z.enum(['en', 'zh']),
});
// Helper function to get audio duration from File
async function getAudioDuration(file: File): Promise<number> {
return new Promise((resolve, reject) => {
const audio = new Audio();
const url = URL.createObjectURL(file);
audio.addEventListener('loadedmetadata', () => {
URL.revokeObjectURL(url);
resolve(audio.duration);
});
audio.addEventListener('error', () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load audio file'));
});
audio.src = url;
});
}
const MAX_AUDIO_DURATION_SECONDS = 30;
const profileSchema = z
.object({
name: z.string().min(1, 'Name is required').max(100),
description: z.string().max(500).optional(),
language: z.enum(['en', 'zh']),
// Sample fields - only required when creating (not editing)
sampleFile: z.instanceof(File).optional(),
referenceText: z.string().max(1000).optional(),
})
.refine(
(data) => {
// If sample file is provided, reference text is required
if (data.sampleFile && (!data.referenceText || data.referenceText.trim().length === 0)) {
return false;
}
return true;
},
{
message: 'Reference text is required when adding a sample',
path: ['referenceText'],
},
);
type ProfileFormValues = z.infer<typeof profileSchema>;
@@ -47,7 +97,13 @@ export function ProfileForm() {
const { data: editingProfile } = useProfile(editingProfileId || '');
const createProfile = useCreateProfile();
const updateProfile = useUpdateProfile();
const addSample = useAddSample();
const transcribe = useTranscription();
const { toast } = useToast();
const [sampleMode, setSampleMode] = useState<'upload' | 'record'>('upload');
const [audioDuration, setAudioDuration] = useState<number | null>(null);
const [isValidatingAudio, setIsValidatingAudio] = useState(false);
const isCreating = !editingProfileId;
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema),
@@ -58,39 +114,213 @@ export function ProfileForm() {
},
});
const selectedFile = form.watch('sampleFile');
// Validate audio duration when file is selected
useEffect(() => {
if (selectedFile && selectedFile instanceof File) {
setIsValidatingAudio(true);
getAudioDuration(selectedFile)
.then((duration) => {
setAudioDuration(duration);
if (duration > MAX_AUDIO_DURATION_SECONDS) {
form.setError('sampleFile', {
type: 'manual',
message: `Audio is too long (${formatAudioDuration(duration)}). Maximum duration is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,
});
} else {
form.clearErrors('sampleFile');
}
})
.catch((error) => {
console.error('Failed to get audio duration:', error);
setAudioDuration(null);
form.setError('sampleFile', {
type: 'manual',
message: 'Failed to validate audio file. Please try a different file.',
});
})
.finally(() => {
setIsValidatingAudio(false);
});
} else {
setAudioDuration(null);
form.clearErrors('sampleFile');
}
}, [selectedFile, form]);
const {
isRecording,
duration,
error: recordingError,
startRecording,
stopRecording,
cancelRecording,
} = useAudioRecording({
maxDurationSeconds: 30,
onRecordingComplete: (blob) => {
const file = new File([blob], `recording-${Date.now()}.webm`, {
type: blob.type || 'audio/webm',
});
form.setValue('sampleFile', file, { shouldValidate: true });
toast({
title: 'Recording complete',
description: 'Audio has been recorded successfully.',
});
},
});
// Show recording errors
useEffect(() => {
if (recordingError) {
toast({
title: 'Recording error',
description: recordingError,
variant: 'destructive',
});
}
}, [recordingError, toast]);
useEffect(() => {
if (editingProfile) {
form.reset({
name: editingProfile.name,
description: editingProfile.description || '',
language: editingProfile.language as 'en' | 'zh',
sampleFile: undefined,
referenceText: undefined,
});
} else {
form.reset({
name: '',
description: '',
language: 'en',
sampleFile: undefined,
referenceText: undefined,
});
setSampleMode('upload');
}
}, [editingProfile, form]);
async function handleTranscribe() {
const file = form.getValues('sampleFile');
if (!file) {
toast({
title: 'No file selected',
description: 'Please select an audio file first.',
variant: 'destructive',
});
return;
}
try {
const language = form.getValues('language') as 'en' | 'zh' | undefined;
const result = await transcribe.mutateAsync({ file, language });
form.setValue('referenceText', result.text, { shouldValidate: true });
toast({
title: 'Transcription complete',
description: 'Audio has been transcribed successfully.',
});
} catch (error) {
toast({
title: 'Transcription failed',
description: error instanceof Error ? error.message : 'Failed to transcribe audio',
variant: 'destructive',
});
}
}
function handleCancelRecording() {
cancelRecording();
form.resetField('sampleFile');
}
async function onSubmit(data: ProfileFormValues) {
try {
if (editingProfileId) {
// Editing: just update profile
await updateProfile.mutateAsync({
profileId: editingProfileId,
data,
data: {
name: data.name,
description: data.description,
language: data.language,
},
});
toast({
title: 'Profile updated',
description: `"${data.name}" has been updated successfully.`,
});
} else {
await createProfile.mutateAsync(data);
toast({
title: 'Profile created',
description: `"${data.name}" has been created successfully.`,
// Get file and reference text directly from form state to ensure we have the values
const sampleFile = form.getValues('sampleFile');
const referenceText = form.getValues('referenceText');
// Validate audio duration before creating profile
if (sampleFile) {
try {
const duration = await getAudioDuration(sampleFile);
if (duration > MAX_AUDIO_DURATION_SECONDS) {
form.setError('sampleFile', {
type: 'manual',
message: `Audio is too long (${formatAudioDuration(duration)}). Maximum duration is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,
});
toast({
title: 'Invalid audio file',
description: `Audio duration is ${formatAudioDuration(duration)}, but maximum is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,
variant: 'destructive',
});
return; // Prevent form submission
}
} catch (error) {
form.setError('sampleFile', {
type: 'manual',
message: 'Failed to validate audio file. Please try a different file.',
});
toast({
title: 'Validation error',
description: error instanceof Error ? error.message : 'Failed to validate audio file',
variant: 'destructive',
});
return; // Prevent form submission
}
}
// Creating: create profile, then optionally add sample
const profile = await createProfile.mutateAsync({
name: data.name,
description: data.description,
language: data.language,
});
// If sample file and reference text provided, add it
if (sampleFile && referenceText && referenceText.trim().length > 0) {
try {
await addSample.mutateAsync({
profileId: profile.id,
file: sampleFile,
referenceText: referenceText,
});
toast({
title: 'Profile created',
description: `"${data.name}" has been created with a sample.`,
});
} catch (sampleError) {
// Profile was created but sample failed - still show success for profile
toast({
title: 'Profile created',
description: `"${data.name}" has been created, but failed to add sample: ${sampleError instanceof Error ? sampleError.message : 'Unknown error'}`,
variant: 'destructive',
});
}
} else {
toast({
title: 'Profile created',
description: `"${data.name}" has been created successfully. You can add samples later.`,
});
}
}
form.reset();
@@ -110,79 +340,309 @@ export function ProfileForm() {
if (!open) {
setEditingProfileId(null);
form.reset();
setSampleMode('upload');
if (isRecording) {
cancelRecording();
}
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>{editingProfileId ? 'Edit Profile' : 'Create Voice Profile'}</DialogTitle>
<DialogDescription>
{editingProfileId
? 'Update your voice profile details.'
: 'Add a new voice profile with samples.'}
: 'Create a new voice profile. You can add a sample now or later.'}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="My Voice" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className={`grid gap-6 ${isCreating ? 'grid-cols-2' : 'grid-cols-1'}`}>
{/* Left column: Profile info */}
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="My Voice" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Describe this voice..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description (Optional)</FormLabel>
<FormControl>
<Textarea placeholder="Describe this voice..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">Chinese</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">Chinese</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex gap-2 justify-end">
{/* Right column: Sample upload section - only show when creating */}
{isCreating && (
<div className="space-y-4 border-l pl-6">
<div>
<h3 className="text-sm font-medium mb-2">Add Sample (Optional)</h3>
<p className="text-sm text-muted-foreground mb-4">
Add an audio sample to get started immediately. You can add more samples
later.
</p>
</div>
<Tabs
value={sampleMode}
onValueChange={(v) => setSampleMode(v as 'upload' | 'record')}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
Upload
</TabsTrigger>
<TabsTrigger value="record" className="flex items-center gap-2">
<Mic className="h-4 w-4" />
Record
</TabsTrigger>
</TabsList>
<TabsContent value="upload" className="space-y-4">
<FormField
control={form.control}
name="sampleFile"
render={({ field: { onChange, name, ref } }) => (
<FormItem>
<FormLabel>Audio File</FormLabel>
<FormControl>
<div className="flex flex-col gap-2">
<Input
type="file"
accept="audio/*"
name={name}
ref={ref}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
onChange(file);
} else {
onChange(undefined);
}
}}
/>
{selectedFile && (
<>
{isValidatingAudio && (
<p className="text-sm text-muted-foreground">
Validating audio...
</p>
)}
{!isValidatingAudio && audioDuration !== null && (
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Duration:</span>
<span
className={
audioDuration > MAX_AUDIO_DURATION_SECONDS
? 'text-destructive font-medium'
: 'text-foreground'
}
>
{formatAudioDuration(audioDuration)}
</span>
<span className="text-muted-foreground">
/ {formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)} max
</span>
</div>
)}
<Button
type="button"
variant="outline"
onClick={handleTranscribe}
disabled={transcribe.isPending || isValidatingAudio || (audioDuration !== null && audioDuration > MAX_AUDIO_DURATION_SECONDS)}
className="flex items-center gap-2 w-full"
>
<Mic className="h-4 w-4" />
{transcribe.isPending ? 'Transcribing...' : 'Transcribe'}
</Button>
</>
)}
</div>
</FormControl>
<FormDescription>
Supported formats: WAV, MP3, M4A. Maximum duration:{' '}
{formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}. Click "Transcribe"
to automatically extract text from the audio.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value="record" className="space-y-4">
<FormField
control={form.control}
name="sampleFile"
render={() => (
<FormItem>
<FormLabel>Record Audio</FormLabel>
<FormControl>
<div className="space-y-4">
{!isRecording && !selectedFile && (
<div className="flex flex-col items-center gap-4 p-4 border-2 border-dashed rounded-lg">
<Button
type="button"
onClick={startRecording}
size="lg"
className="flex items-center gap-2"
>
<Mic className="h-5 w-5" />
Start Recording
</Button>
<p className="text-sm text-muted-foreground text-center">
Click to start recording. Maximum duration: 30 seconds.
</p>
</div>
)}
{isRecording && (
<div className="flex flex-col items-center gap-4 p-4 border-2 border-destructive rounded-lg bg-destructive/5">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-destructive animate-pulse" />
<span className="text-lg font-mono font-semibold">
{formatAudioDuration(duration)}
</span>
</div>
</div>
<Button
type="button"
onClick={stopRecording}
variant="destructive"
className="flex items-center gap-2"
>
<Square className="h-4 w-4" />
Stop Recording
</Button>
<p className="text-sm text-muted-foreground text-center">
Recording in progress... ({formatAudioDuration(30 - duration)}{' '}
remaining)
</p>
</div>
)}
{selectedFile && !isRecording && (
<div className="flex flex-col items-center gap-4 p-4 border-2 border-primary rounded-lg bg-primary/5">
<div className="flex items-center gap-2">
<Mic className="h-5 w-5 text-primary" />
<span className="font-medium">Recording complete</span>
</div>
<p className="text-sm text-muted-foreground text-center">
File: {selectedFile.name}
</p>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
onClick={handleTranscribe}
disabled={transcribe.isPending}
className="flex items-center gap-2"
>
<Mic className="h-4 w-4" />
{transcribe.isPending ? 'Transcribing...' : 'Transcribe'}
</Button>
<Button
type="button"
variant="outline"
onClick={handleCancelRecording}
className="flex items-center gap-2"
>
Record Again
</Button>
</div>
</div>
)}
</div>
</FormControl>
<FormDescription>
Record audio directly from your microphone. Maximum duration is 30
seconds.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</Tabs>
<FormField
control={form.control}
name="referenceText"
render={({ field }) => (
<FormItem>
<FormLabel>Reference Text</FormLabel>
<FormControl>
<Textarea
placeholder="Enter the exact text spoken in the audio..."
className="min-h-[100px]"
{...field}
/>
</FormControl>
<FormDescription>
This should match exactly what is spoken in the audio file. Required if
you add a sample.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
<div className="flex gap-2 justify-end mt-6 pt-4 border-t">
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={createProfile.isPending || updateProfile.isPending}>
{createProfile.isPending || updateProfile.isPending
<Button
type="submit"
disabled={createProfile.isPending || updateProfile.isPending || addSample.isPending}
>
{createProfile.isPending || updateProfile.isPending || addSample.isPending
? 'Saving...'
: editingProfileId
? 'Update Profile'
@@ -66,9 +66,7 @@ export function SampleList({ profileId }: SampleListProps) {
variant="ghost"
size="sm"
onClick={() => handlePlay(sample.audio_path, sample.reference_text, sample.id)}
className={
currentAudioId === sample.id && isPlaying ? 'text-primary' : ''
}
className={currentAudioId === sample.id && isPlaying ? 'text-primary' : ''}
>
<Play className="h-4 w-4 mr-1" />
Play
@@ -108,9 +108,9 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
try {
const language = profile?.language as 'en' | 'zh' | undefined;
const result = await transcribe.mutateAsync({ file, language });
form.setValue('referenceText', result.text, { shouldValidate: true });
toast({
title: 'Transcription complete',
description: 'Audio has been transcribed successfully.',
@@ -223,7 +223,8 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
</div>
</FormControl>
<FormDescription>
Supported formats: WAV, MP3, M4A. Click "Transcribe" to automatically extract text from the audio.
Supported formats: WAV, MP3, M4A. Click "Transcribe" to automatically
extract text from the audio.
</FormDescription>
<FormMessage />
</FormItem>
@@ -277,7 +278,8 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
Stop Recording
</Button>
<p className="text-sm text-muted-foreground text-center">
Recording in progress... ({formatAudioDuration(30 - duration)} remaining)
Recording in progress... ({formatAudioDuration(30 - duration)}{' '}
remaining)
</p>
</div>
)}
+3 -4
View File
@@ -1,8 +1,7 @@
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface CircleButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
export interface CircleButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: React.ComponentType<{ className?: string }>;
}
@@ -16,14 +15,14 @@ const CircleButton = React.forwardRef<HTMLButtonElement, CircleButtonProps>(
'hover:bg-accent transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
className
className,
)}
{...props}
>
<Icon className="h-3.5 w-3.5 text-muted-foreground/60" />
</button>
);
}
},
);
CircleButton.displayName = 'CircleButton';
+1 -4
View File
@@ -8,10 +8,7 @@ const Slider = React.forwardRef<
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className,
)}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
+10 -2
View File
@@ -11,6 +11,10 @@ export interface UpdateStatus {
error?: string;
}
const isTauri = () => {
return '__TAURI_INTERNALS__' in window;
};
export function useAutoUpdater(checkOnMount = false) {
const [status, setStatus] = useState<UpdateStatus>({
checking: false,
@@ -22,6 +26,10 @@ export function useAutoUpdater(checkOnMount = false) {
const [update, setUpdate] = useState<Update | null>(null);
const checkForUpdates = async () => {
if (!isTauri()) {
return;
}
try {
setStatus((prev) => ({ ...prev, checking: true, error: undefined }));
@@ -56,7 +64,7 @@ export function useAutoUpdater(checkOnMount = false) {
};
const downloadAndInstall = async () => {
if (!update) return;
if (!update || !isTauri()) return;
try {
setStatus((prev) => ({ ...prev, downloading: true, error: undefined }));
@@ -91,7 +99,7 @@ export function useAutoUpdater(checkOnMount = false) {
};
useEffect(() => {
if (checkOnMount) {
if (checkOnMount && isTauri()) {
checkForUpdates();
}
}, [checkOnMount]);
+6 -21
View File
@@ -20,10 +20,7 @@ class ApiClient {
return serverUrl;
}
private async request<T>(
endpoint: string,
options?: RequestInit,
): Promise<T> {
private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = `${this.getBaseUrl()}${endpoint}`;
const response = await fetch(url, {
...options,
@@ -64,10 +61,7 @@ class ApiClient {
return this.request<VoiceProfileResponse>(`/profiles/${profileId}`);
}
async updateProfile(
profileId: string,
data: VoiceProfileCreate,
): Promise<VoiceProfileResponse> {
async updateProfile(profileId: string, data: VoiceProfileCreate): Promise<VoiceProfileResponse> {
return this.request<VoiceProfileResponse>(`/profiles/${profileId}`, {
method: 'PUT',
body: JSON.stringify(data),
@@ -105,12 +99,8 @@ class ApiClient {
return response.json();
}
async listProfileSamples(
profileId: string,
): Promise<ProfileSampleResponse[]> {
return this.request<ProfileSampleResponse[]>(
`/profiles/${profileId}/samples`,
);
async listProfileSamples(profileId: string): Promise<ProfileSampleResponse[]> {
return this.request<ProfileSampleResponse[]>(`/profiles/${profileId}/samples`);
}
async deleteProfileSample(sampleId: string): Promise<void> {
@@ -120,9 +110,7 @@ class ApiClient {
}
// Generation
async generateSpeech(
data: GenerationRequest,
): Promise<GenerationResponse> {
async generateSpeech(data: GenerationRequest): Promise<GenerationResponse> {
return this.request<GenerationResponse>('/generate', {
method: 'POST',
body: JSON.stringify(data),
@@ -159,10 +147,7 @@ class ApiClient {
}
// Transcription
async transcribeAudio(
file: File,
language?: 'en' | 'zh',
): Promise<TranscriptionResponse> {
async transcribeAudio(file: File, language?: 'en' | 'zh'): Promise<TranscriptionResponse> {
const formData = new FormData();
formData.append('file', file);
if (language) {
+14 -14
View File
@@ -6,20 +6,20 @@ import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
+11 -11
View File
@@ -3,15 +3,15 @@
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
+5 -5
View File
@@ -3,9 +3,9 @@
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};
+110 -111
View File
@@ -3,129 +3,128 @@
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel,
) => void,
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
public get isCancelled(): boolean {
return this.#isCancelled;
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return 'Cancellable Promise';
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}
+18 -18
View File
@@ -8,25 +8,25 @@ type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
VERSION: '0.1.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
BASE: '',
VERSION: '0.1.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};
+247 -228
View File
@@ -9,278 +9,294 @@ import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
export const isDefined = <T>(
value: T | null | undefined,
): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach((v) => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
return '';
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
export const resolve = async <T>(
options: ApiRequestOptions,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
export const getHeaders = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>,
);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new Headers(headers);
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body);
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
return undefined;
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel,
): Promise<Response> => {
const controller = new AbortController();
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
onCancel(() => controller.abort());
return await fetch(url, request);
return await fetch(url, request);
};
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
export const getResponseHeader = (
response: Response,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
return undefined;
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json']
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json'];
const isJSON = jsonTypes.some((type) => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
return undefined;
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
};
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
);
}
};
/**
@@ -290,33 +306,36 @@ export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult):
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};
@@ -3,7 +3,6 @@
/* tslint:disable */
/* eslint-disable */
export type Body_add_profile_sample_profiles__profile_id__samples_post = {
file: Blob;
reference_text: string;
file: Blob;
reference_text: string;
};
@@ -3,7 +3,6 @@
/* tslint:disable */
/* eslint-disable */
export type Body_transcribe_audio_transcribe_post = {
file: Blob;
language?: (string | null);
file: Blob;
language?: string | null;
};
+6 -6
View File
@@ -6,10 +6,10 @@
* Request model for voice generation.
*/
export type GenerationRequest = {
profile_id: string;
text: string;
language?: string;
seed?: (number | null);
model_size?: (string | null);
profile_id: string;
text: string;
language?: string;
seed?: number | null;
model_size?: string | null;
instruct?: string | null;
};
+9 -9
View File
@@ -6,13 +6,13 @@
* Response model for voice generation.
*/
export type GenerationResponse = {
id: string;
profile_id: string;
text: string;
language: string;
audio_path: string;
duration: number;
seed: (number | null);
created_at: string;
id: string;
profile_id: string;
text: string;
language: string;
audio_path: string;
duration: number;
seed: number | null;
instruct: string | null;
created_at: string;
};
@@ -4,6 +4,5 @@
/* eslint-disable */
import type { ValidationError } from './ValidationError';
export type HTTPValidationError = {
detail?: Array<ValidationError>;
detail?: Array<ValidationError>;
};
+6 -7
View File
@@ -6,11 +6,10 @@
* Response model for health check.
*/
export type HealthResponse = {
status: string;
model_loaded: boolean;
model_downloaded?: (boolean | null);
model_size?: (string | null);
gpu_available: boolean;
vram_used_mb?: (number | null);
status: string;
model_loaded: boolean;
model_downloaded?: boolean | null;
model_size?: string | null;
gpu_available: boolean;
vram_used_mb?: number | null;
};
@@ -7,7 +7,6 @@ import type { HistoryResponse } from './HistoryResponse';
* Response model for history list.
*/
export type HistoryListResponse = {
items: Array<HistoryResponse>;
total: number;
items: Array<HistoryResponse>;
total: number;
};
+10 -10
View File
@@ -6,14 +6,14 @@
* Response model for history entry (includes profile name).
*/
export type HistoryResponse = {
id: string;
profile_id: string;
profile_name: string;
text: string;
language: string;
audio_path: string;
duration: number;
seed: (number | null);
created_at: string;
id: string;
profile_id: string;
profile_name: string;
text: string;
language: string;
audio_path: string;
duration: number;
seed: number | null;
instruct: string | null;
created_at: string;
};
@@ -6,6 +6,5 @@
* Request model for triggering model download.
*/
export type ModelDownloadRequest = {
model_name: string;
model_name: string;
};
+5 -6
View File
@@ -6,10 +6,9 @@
* Response model for model status.
*/
export type ModelStatus = {
model_name: string;
display_name: string;
downloaded: boolean;
size_mb?: (number | null);
loaded?: boolean;
model_name: string;
display_name: string;
downloaded: boolean;
size_mb?: number | null;
loaded?: boolean;
};
@@ -7,6 +7,5 @@ import type { ModelStatus } from './ModelStatus';
* Response model for model status list.
*/
export type ModelStatusListResponse = {
models: Array<ModelStatus>;
models: Array<ModelStatus>;
};
@@ -6,9 +6,8 @@
* Response model for profile sample.
*/
export type ProfileSampleResponse = {
id: string;
profile_id: string;
audio_path: string;
reference_text: string;
id: string;
profile_id: string;
audio_path: string;
reference_text: string;
};
@@ -6,7 +6,6 @@
* Response model for transcription.
*/
export type TranscriptionResponse = {
text: string;
duration: number;
text: string;
duration: number;
};
+3 -4
View File
@@ -3,8 +3,7 @@
/* tslint:disable */
/* eslint-disable */
export type ValidationError = {
loc: Array<(string | number)>;
msg: string;
type: string;
loc: Array<string | number>;
msg: string;
type: string;
};
+3 -4
View File
@@ -6,8 +6,7 @@
* Request model for creating a voice profile.
*/
export type VoiceProfileCreate = {
name: string;
description?: (string | null);
language?: string;
name: string;
description?: string | null;
language?: string;
};
@@ -6,11 +6,10 @@
* Response model for voice profile.
*/
export type VoiceProfileResponse = {
id: string;
name: string;
description: (string | null);
language: string;
created_at: string;
updated_at: string;
id: string;
name: string;
description: string | null;
language: string;
created_at: string;
updated_at: string;
};
@@ -3,15 +3,15 @@
/* tslint:disable */
/* eslint-disable */
export const $Body_add_profile_sample_profiles__profile_id__samples_post = {
properties: {
file: {
type: 'binary',
isRequired: true,
format: 'binary',
},
reference_text: {
type: 'string',
isRequired: true,
},
properties: {
file: {
type: 'binary',
isRequired: true,
format: 'binary',
},
reference_text: {
type: 'string',
isRequired: true,
},
},
} as const;
@@ -3,19 +3,22 @@
/* tslint:disable */
/* eslint-disable */
export const $Body_transcribe_audio_transcribe_post = {
properties: {
file: {
type: 'binary',
isRequired: true,
format: 'binary',
},
language: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
properties: {
file: {
type: 'binary',
isRequired: true,
format: 'binary',
},
language: {
type: 'any-of',
contains: [
{
type: 'string',
},
{
type: 'null',
},
],
},
},
} as const;
+39 -33
View File
@@ -3,38 +3,44 @@
/* tslint:disable */
/* eslint-disable */
export const $GenerationRequest = {
description: `Request model for voice generation.`,
properties: {
profile_id: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
maxLength: 5000,
minLength: 1,
},
language: {
type: 'string',
pattern: '^(en|zh)$',
},
seed: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
},
model_size: {
type: 'any-of',
contains: [{
type: 'string',
pattern: '^(1\\.7B|0\\.6B)$',
}, {
type: 'null',
}],
},
description: `Request model for voice generation.`,
properties: {
profile_id: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
maxLength: 5000,
minLength: 1,
},
language: {
type: 'string',
pattern: '^(en|zh)$',
},
seed: {
type: 'any-of',
contains: [
{
type: 'number',
},
{
type: 'null',
},
],
},
model_size: {
type: 'any-of',
contains: [
{
type: 'string',
pattern: '^(1\\.7B|0\\.6B)$',
},
{
type: 'null',
},
],
},
},
} as const;
+43 -40
View File
@@ -3,45 +3,48 @@
/* tslint:disable */
/* eslint-disable */
export const $GenerationResponse = {
description: `Response model for voice generation.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
seed: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
description: `Response model for voice generation.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
seed: {
type: 'any-of',
contains: [
{
type: 'number',
},
{
type: 'null',
},
],
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
},
} as const;
@@ -3,12 +3,12 @@
/* tslint:disable */
/* eslint-disable */
export const $HTTPValidationError = {
properties: {
detail: {
type: 'array',
contains: {
type: 'ValidationError',
},
},
properties: {
detail: {
type: 'array',
contains: {
type: 'ValidationError',
},
},
},
} as const;
+47 -38
View File
@@ -3,43 +3,52 @@
/* tslint:disable */
/* eslint-disable */
export const $HealthResponse = {
description: `Response model for health check.`,
properties: {
status: {
type: 'string',
isRequired: true,
},
model_loaded: {
type: 'boolean',
isRequired: true,
},
model_downloaded: {
type: 'any-of',
contains: [{
type: 'boolean',
}, {
type: 'null',
}],
},
model_size: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
gpu_available: {
type: 'boolean',
isRequired: true,
},
vram_used_mb: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
},
description: `Response model for health check.`,
properties: {
status: {
type: 'string',
isRequired: true,
},
model_loaded: {
type: 'boolean',
isRequired: true,
},
model_downloaded: {
type: 'any-of',
contains: [
{
type: 'boolean',
},
{
type: 'null',
},
],
},
model_size: {
type: 'any-of',
contains: [
{
type: 'string',
},
{
type: 'null',
},
],
},
gpu_available: {
type: 'boolean',
isRequired: true,
},
vram_used_mb: {
type: 'any-of',
contains: [
{
type: 'number',
},
{
type: 'null',
},
],
},
},
} as const;
+13 -13
View File
@@ -3,18 +3,18 @@
/* tslint:disable */
/* eslint-disable */
export const $HistoryListResponse = {
description: `Response model for history list.`,
properties: {
items: {
type: 'array',
contains: {
type: 'HistoryResponse',
},
isRequired: true,
},
total: {
type: 'number',
isRequired: true,
},
description: `Response model for history list.`,
properties: {
items: {
type: 'array',
contains: {
type: 'HistoryResponse',
},
isRequired: true,
},
total: {
type: 'number',
isRequired: true,
},
},
} as const;
+47 -44
View File
@@ -3,49 +3,52 @@
/* tslint:disable */
/* eslint-disable */
export const $HistoryResponse = {
description: `Response model for history entry (includes profile name).`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
profile_name: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
seed: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
description: `Response model for history entry (includes profile name).`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
profile_name: {
type: 'string',
isRequired: true,
},
text: {
type: 'string',
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
seed: {
type: 'any-of',
contains: [
{
type: 'number',
},
{
type: 'null',
},
],
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
},
} as const;
@@ -3,11 +3,11 @@
/* tslint:disable */
/* eslint-disable */
export const $ModelDownloadRequest = {
description: `Request model for triggering model download.`,
properties: {
model_name: {
type: 'string',
isRequired: true,
},
description: `Request model for triggering model download.`,
properties: {
model_name: {
type: 'string',
isRequired: true,
},
},
} as const;
+28 -25
View File
@@ -3,30 +3,33 @@
/* tslint:disable */
/* eslint-disable */
export const $ModelStatus = {
description: `Response model for model status.`,
properties: {
model_name: {
type: 'string',
isRequired: true,
},
display_name: {
type: 'string',
isRequired: true,
},
downloaded: {
type: 'boolean',
isRequired: true,
},
size_mb: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
},
loaded: {
type: 'boolean',
},
description: `Response model for model status.`,
properties: {
model_name: {
type: 'string',
isRequired: true,
},
display_name: {
type: 'string',
isRequired: true,
},
downloaded: {
type: 'boolean',
isRequired: true,
},
size_mb: {
type: 'any-of',
contains: [
{
type: 'number',
},
{
type: 'null',
},
],
},
loaded: {
type: 'boolean',
},
},
} as const;
@@ -3,14 +3,14 @@
/* tslint:disable */
/* eslint-disable */
export const $ModelStatusListResponse = {
description: `Response model for model status list.`,
properties: {
models: {
type: 'array',
contains: {
type: 'ModelStatus',
},
isRequired: true,
},
description: `Response model for model status list.`,
properties: {
models: {
type: 'array',
contains: {
type: 'ModelStatus',
},
isRequired: true,
},
},
} as const;
@@ -3,23 +3,23 @@
/* tslint:disable */
/* eslint-disable */
export const $ProfileSampleResponse = {
description: `Response model for profile sample.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
reference_text: {
type: 'string',
isRequired: true,
},
description: `Response model for profile sample.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
profile_id: {
type: 'string',
isRequired: true,
},
audio_path: {
type: 'string',
isRequired: true,
},
reference_text: {
type: 'string',
isRequired: true,
},
},
} as const;
@@ -3,15 +3,15 @@
/* tslint:disable */
/* eslint-disable */
export const $TranscriptionResponse = {
description: `Response model for transcription.`,
properties: {
text: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
description: `Response model for transcription.`,
properties: {
text: {
type: 'string',
isRequired: true,
},
duration: {
type: 'number',
isRequired: true,
},
},
} as const;
+23 -20
View File
@@ -3,26 +3,29 @@
/* tslint:disable */
/* eslint-disable */
export const $ValidationError = {
properties: {
loc: {
type: 'array',
contains: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'number',
}],
},
isRequired: true,
},
msg: {
properties: {
loc: {
type: 'array',
contains: {
type: 'any-of',
contains: [
{
type: 'string',
isRequired: true,
},
type: {
type: 'string',
isRequired: true,
},
},
{
type: 'number',
},
],
},
isRequired: true,
},
msg: {
type: 'string',
isRequired: true,
},
type: {
type: 'string',
isRequired: true,
},
},
} as const;
+24 -21
View File
@@ -3,26 +3,29 @@
/* tslint:disable */
/* eslint-disable */
export const $VoiceProfileCreate = {
description: `Request model for creating a voice profile.`,
properties: {
name: {
type: 'string',
isRequired: true,
maxLength: 100,
minLength: 1,
},
description: {
type: 'any-of',
contains: [{
type: 'string',
maxLength: 500,
}, {
type: 'null',
}],
},
language: {
type: 'string',
pattern: '^(en|zh)$',
},
description: `Request model for creating a voice profile.`,
properties: {
name: {
type: 'string',
isRequired: true,
maxLength: 100,
minLength: 1,
},
description: {
type: 'any-of',
contains: [
{
type: 'string',
maxLength: 500,
},
{
type: 'null',
},
],
},
language: {
type: 'string',
pattern: '^(en|zh)$',
},
},
} as const;
@@ -3,38 +3,41 @@
/* tslint:disable */
/* eslint-disable */
export const $VoiceProfileResponse = {
description: `Response model for voice profile.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
name: {
type: 'string',
isRequired: true,
},
description: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
updated_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
description: `Response model for voice profile.`,
properties: {
id: {
type: 'string',
isRequired: true,
},
name: {
type: 'string',
isRequired: true,
},
description: {
type: 'any-of',
contains: [
{
type: 'string',
},
{
type: 'null',
},
],
isRequired: true,
},
language: {
type: 'string',
isRequired: true,
},
created_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
updated_at: {
type: 'string',
isRequired: true,
format: 'date-time',
},
},
} as const;
+437 -437
View File
@@ -19,441 +19,441 @@ import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class DefaultService {
/**
* Root
* Root endpoint.
* @returns any Successful Response
* @throws ApiError
*/
public static rootGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/',
});
}
/**
* Health
* Health check endpoint.
* @returns HealthResponse Successful Response
* @throws ApiError
*/
public static healthHealthGet(): CancelablePromise<HealthResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/health',
});
}
/**
* List Profiles
* List all voice profiles.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static listProfilesProfilesGet(): CancelablePromise<Array<VoiceProfileResponse>> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles',
});
}
/**
* Create Profile
* Create a new voice profile.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static createProfileProfilesPost({
requestBody,
}: {
requestBody: VoiceProfileCreate,
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/profiles',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Profile
* Get a voice profile by ID.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static getProfileProfilesProfileIdGet({
profileId,
}: {
profileId: string,
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles/{profile_id}',
path: {
'profile_id': profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Profile
* Update a voice profile.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static updateProfileProfilesProfileIdPut({
profileId,
requestBody,
}: {
profileId: string,
requestBody: VoiceProfileCreate,
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/profiles/{profile_id}',
path: {
'profile_id': profileId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Profile
* Delete a voice profile.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteProfileProfilesProfileIdDelete({
profileId,
}: {
profileId: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/profiles/{profile_id}',
path: {
'profile_id': profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Add Profile Sample
* Add a sample to a voice profile.
* @returns ProfileSampleResponse Successful Response
* @throws ApiError
*/
public static addProfileSampleProfilesProfileIdSamplesPost({
profileId,
formData,
}: {
profileId: string,
formData: Body_add_profile_sample_profiles__profile_id__samples_post,
}): CancelablePromise<ProfileSampleResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/profiles/{profile_id}/samples',
path: {
'profile_id': profileId,
},
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Profile Samples
* Get all samples for a profile.
* @returns ProfileSampleResponse Successful Response
* @throws ApiError
*/
public static getProfileSamplesProfilesProfileIdSamplesGet({
profileId,
}: {
profileId: string,
}): CancelablePromise<Array<ProfileSampleResponse>> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles/{profile_id}/samples',
path: {
'profile_id': profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Profile Sample
* Delete a profile sample.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteProfileSampleProfilesSamplesSampleIdDelete({
sampleId,
}: {
sampleId: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/profiles/samples/{sample_id}',
path: {
'sample_id': sampleId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Generate Speech
* Generate speech from text using a voice profile.
* @returns GenerationResponse Successful Response
* @throws ApiError
*/
public static generateSpeechGeneratePost({
requestBody,
}: {
requestBody: GenerationRequest,
}): CancelablePromise<GenerationResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/generate',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* List History
* List generation history with optional filters.
* @returns HistoryListResponse Successful Response
* @throws ApiError
*/
public static listHistoryHistoryGet({
profileId,
search,
limit = 50,
offset,
}: {
profileId?: (string | null),
search?: (string | null),
limit?: number,
offset?: number,
}): CancelablePromise<HistoryListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/history',
query: {
'profile_id': profileId,
'search': search,
'limit': limit,
'offset': offset,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Generation
* Get a generation by ID.
* @returns HistoryResponse Successful Response
* @throws ApiError
*/
public static getGenerationHistoryGenerationIdGet({
generationId,
}: {
generationId: string,
}): CancelablePromise<HistoryResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/history/{generation_id}',
path: {
'generation_id': generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Generation
* Delete a generation.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteGenerationHistoryGenerationIdDelete({
generationId,
}: {
generationId: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/history/{generation_id}',
path: {
'generation_id': generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Stats
* Get generation statistics.
* @returns any Successful Response
* @throws ApiError
*/
public static getStatsHistoryStatsGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/history/stats',
});
}
/**
* Transcribe Audio
* Transcribe audio file to text.
* @returns TranscriptionResponse Successful Response
* @throws ApiError
*/
public static transcribeAudioTranscribePost({
formData,
}: {
formData: Body_transcribe_audio_transcribe_post,
}): CancelablePromise<TranscriptionResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/transcribe',
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Audio
* Serve generated audio file.
* @returns any Successful Response
* @throws ApiError
*/
public static getAudioAudioGenerationIdGet({
generationId,
}: {
generationId: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/audio/{generation_id}',
path: {
'generation_id': generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Load Model
* Manually load TTS model.
* @returns any Successful Response
* @throws ApiError
*/
public static loadModelModelsLoadPost({
modelSize = '1.7B',
}: {
modelSize?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/load',
query: {
'model_size': modelSize,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Unload Model
* Unload TTS model to free memory.
* @returns any Successful Response
* @throws ApiError
*/
public static unloadModelModelsUnloadPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/unload',
});
}
/**
* Get Model Progress
* Get model download progress via Server-Sent Events.
* @returns any Successful Response
* @throws ApiError
*/
public static getModelProgressModelsProgressModelNameGet({
modelName,
}: {
modelName: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/models/progress/{model_name}',
path: {
'model_name': modelName,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Model Status
* Get status of all available models.
* @returns ModelStatusListResponse Successful Response
* @throws ApiError
*/
public static getModelStatusModelsStatusGet(): CancelablePromise<ModelStatusListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/models/status',
});
}
/**
* Trigger Model Download
* Trigger download of a specific model.
* @returns any Successful Response
* @throws ApiError
*/
public static triggerModelDownloadModelsDownloadPost({
requestBody,
}: {
requestBody: ModelDownloadRequest,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/download',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Root
* Root endpoint.
* @returns any Successful Response
* @throws ApiError
*/
public static rootGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/',
});
}
/**
* Health
* Health check endpoint.
* @returns HealthResponse Successful Response
* @throws ApiError
*/
public static healthHealthGet(): CancelablePromise<HealthResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/health',
});
}
/**
* List Profiles
* List all voice profiles.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static listProfilesProfilesGet(): CancelablePromise<Array<VoiceProfileResponse>> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles',
});
}
/**
* Create Profile
* Create a new voice profile.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static createProfileProfilesPost({
requestBody,
}: {
requestBody: VoiceProfileCreate;
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/profiles',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Profile
* Get a voice profile by ID.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static getProfileProfilesProfileIdGet({
profileId,
}: {
profileId: string;
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles/{profile_id}',
path: {
profile_id: profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Profile
* Update a voice profile.
* @returns VoiceProfileResponse Successful Response
* @throws ApiError
*/
public static updateProfileProfilesProfileIdPut({
profileId,
requestBody,
}: {
profileId: string;
requestBody: VoiceProfileCreate;
}): CancelablePromise<VoiceProfileResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/profiles/{profile_id}',
path: {
profile_id: profileId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Profile
* Delete a voice profile.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteProfileProfilesProfileIdDelete({
profileId,
}: {
profileId: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/profiles/{profile_id}',
path: {
profile_id: profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Add Profile Sample
* Add a sample to a voice profile.
* @returns ProfileSampleResponse Successful Response
* @throws ApiError
*/
public static addProfileSampleProfilesProfileIdSamplesPost({
profileId,
formData,
}: {
profileId: string;
formData: Body_add_profile_sample_profiles__profile_id__samples_post;
}): CancelablePromise<ProfileSampleResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/profiles/{profile_id}/samples',
path: {
profile_id: profileId,
},
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Profile Samples
* Get all samples for a profile.
* @returns ProfileSampleResponse Successful Response
* @throws ApiError
*/
public static getProfileSamplesProfilesProfileIdSamplesGet({
profileId,
}: {
profileId: string;
}): CancelablePromise<Array<ProfileSampleResponse>> {
return __request(OpenAPI, {
method: 'GET',
url: '/profiles/{profile_id}/samples',
path: {
profile_id: profileId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Profile Sample
* Delete a profile sample.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteProfileSampleProfilesSamplesSampleIdDelete({
sampleId,
}: {
sampleId: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/profiles/samples/{sample_id}',
path: {
sample_id: sampleId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Generate Speech
* Generate speech from text using a voice profile.
* @returns GenerationResponse Successful Response
* @throws ApiError
*/
public static generateSpeechGeneratePost({
requestBody,
}: {
requestBody: GenerationRequest;
}): CancelablePromise<GenerationResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/generate',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* List History
* List generation history with optional filters.
* @returns HistoryListResponse Successful Response
* @throws ApiError
*/
public static listHistoryHistoryGet({
profileId,
search,
limit = 50,
offset,
}: {
profileId?: string | null;
search?: string | null;
limit?: number;
offset?: number;
}): CancelablePromise<HistoryListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/history',
query: {
profile_id: profileId,
search: search,
limit: limit,
offset: offset,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Generation
* Get a generation by ID.
* @returns HistoryResponse Successful Response
* @throws ApiError
*/
public static getGenerationHistoryGenerationIdGet({
generationId,
}: {
generationId: string;
}): CancelablePromise<HistoryResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/history/{generation_id}',
path: {
generation_id: generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Generation
* Delete a generation.
* @returns any Successful Response
* @throws ApiError
*/
public static deleteGenerationHistoryGenerationIdDelete({
generationId,
}: {
generationId: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/history/{generation_id}',
path: {
generation_id: generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Stats
* Get generation statistics.
* @returns any Successful Response
* @throws ApiError
*/
public static getStatsHistoryStatsGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/history/stats',
});
}
/**
* Transcribe Audio
* Transcribe audio file to text.
* @returns TranscriptionResponse Successful Response
* @throws ApiError
*/
public static transcribeAudioTranscribePost({
formData,
}: {
formData: Body_transcribe_audio_transcribe_post;
}): CancelablePromise<TranscriptionResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/transcribe',
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Audio
* Serve generated audio file.
* @returns any Successful Response
* @throws ApiError
*/
public static getAudioAudioGenerationIdGet({
generationId,
}: {
generationId: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/audio/{generation_id}',
path: {
generation_id: generationId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Load Model
* Manually load TTS model.
* @returns any Successful Response
* @throws ApiError
*/
public static loadModelModelsLoadPost({
modelSize = '1.7B',
}: {
modelSize?: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/load',
query: {
model_size: modelSize,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Unload Model
* Unload TTS model to free memory.
* @returns any Successful Response
* @throws ApiError
*/
public static unloadModelModelsUnloadPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/unload',
});
}
/**
* Get Model Progress
* Get model download progress via Server-Sent Events.
* @returns any Successful Response
* @throws ApiError
*/
public static getModelProgressModelsProgressModelNameGet({
modelName,
}: {
modelName: string;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/models/progress/{model_name}',
path: {
model_name: modelName,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Model Status
* Get status of all available models.
* @returns ModelStatusListResponse Successful Response
* @throws ApiError
*/
public static getModelStatusModelsStatusGet(): CancelablePromise<ModelStatusListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/models/status',
});
}
/**
* Trigger Model Download
* Trigger download of a specific model.
* @returns any Successful Response
* @throws ApiError
*/
public static triggerModelDownloadModelsDownloadPost({
requestBody,
}: {
requestBody: ModelDownloadRequest;
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/models/download',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}
+5 -4
View File
@@ -28,7 +28,8 @@ export function useAudioRecording({
// Check if getUserMedia is available
// In Tauri, navigator.mediaDevices might not be available immediately
if (typeof navigator === 'undefined') {
const errorMsg = 'Navigator API is not available. This might be a Tauri configuration issue.';
const errorMsg =
'Navigator API is not available. This might be a Tauri configuration issue.';
setError(errorMsg);
throw new Error(errorMsg);
}
@@ -36,7 +37,7 @@ export function useAudioRecording({
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
// Try waiting a bit for Tauri webview to initialize
await new Promise((resolve) => setTimeout(resolve, 100));
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
const isTauriEnv = isTauri();
console.error('MediaDevices check:', {
@@ -45,7 +46,7 @@ export function useAudioRecording({
hasGetUserMedia: !!navigator?.mediaDevices?.getUserMedia,
isTauri: isTauriEnv,
});
const errorMsg = isTauriEnv
? 'Microphone access is not available. Please ensure:\n1. The app has microphone permissions in System Settings (macOS: System Settings > Privacy & Security > Microphone)\n2. You restart the app after granting permissions\n3. You are using Tauri v2 with a webview that supports getUserMedia'
: 'Microphone access is not available. Please ensure you are using a secure context (HTTPS or localhost) and that your browser has microphone permissions enabled.';
@@ -87,7 +88,7 @@ export function useAudioRecording({
mediaRecorder.onstop = () => {
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
onRecordingComplete?.(blob);
// Stop all tracks
streamRef.current?.getTracks().forEach((track) => {
track.stop();
+1 -2
View File
@@ -28,8 +28,7 @@ export const useServerStore = create<ServerStore>()(
setMode: (mode) => set({ mode }),
keepServerRunningOnClose: false,
setKeepServerRunningOnClose: (keepRunning) =>
set({ keepServerRunningOnClose: keepRunning }),
setKeepServerRunningOnClose: (keepRunning) => set({ keepServerRunningOnClose: keepRunning }),
}),
{
name: 'voicebox-server',