feat: allow multiple recording file upload

This commit is contained in:
Abhishek Kumar 2026-04-06 16:50:24 +05:30
parent 95d6dd44ff
commit 6792ecd301
7 changed files with 2323 additions and 2978 deletions

View file

@ -10,10 +10,12 @@ from api.db import db_client
from api.db.workflow_recording_client import generate_short_id
from api.enums import StorageBackend
from api.schemas.workflow_recording import (
RecordingCreateRequestSchema,
BatchRecordingCreateRequestSchema,
BatchRecordingCreateResponseSchema,
BatchRecordingUploadRequestSchema,
BatchRecordingUploadResponseSchema,
RecordingListResponseSchema,
RecordingResponseSchema,
RecordingUploadRequestSchema,
RecordingUploadResponseSchema,
)
from api.services.auth.depends import get_user
@ -56,94 +58,105 @@ def _build_response(rec) -> RecordingResponseSchema:
@router.post(
"/upload-url",
response_model=RecordingUploadResponseSchema,
summary="Get presigned URL for recording upload",
response_model=BatchRecordingUploadResponseSchema,
summary="Get presigned URLs for recording uploads",
)
async def get_upload_url(
request: RecordingUploadRequestSchema,
async def get_upload_urls(
request: BatchRecordingUploadRequestSchema,
user=Depends(get_user),
):
"""Generate a presigned PUT URL for uploading an audio recording."""
"""Generate presigned PUT URLs for uploading one or more audio recordings."""
try:
recording_id = await _generate_unique_recording_id()
items = []
for fd in request.files:
recording_id = await _generate_unique_recording_id()
storage_key = (
f"recordings/{user.selected_organization_id}"
f"/{request.workflow_id}/{recording_id}"
f"/{request.filename}"
)
storage_key = (
f"recordings/{user.selected_organization_id}"
f"/{request.workflow_id}/{recording_id}"
f"/{fd.filename}"
)
upload_url = await storage_fs.aget_presigned_put_url(
file_path=storage_key,
expiration=1800, # 30 minutes
content_type=request.mime_type,
max_size=5_242_880, # 5MB max
)
upload_url = await storage_fs.aget_presigned_put_url(
file_path=storage_key,
expiration=1800,
content_type=fd.mime_type,
max_size=5_242_880,
)
if not upload_url:
raise HTTPException(
status_code=500, detail="Failed to generate presigned upload URL"
if not upload_url:
raise HTTPException(
status_code=500,
detail=f"Failed to generate presigned upload URL for {fd.filename}",
)
items.append(
RecordingUploadResponseSchema(
upload_url=upload_url,
recording_id=recording_id,
storage_key=storage_key,
)
)
logger.info(
f"Generated recording upload URL: {recording_id}, "
f"Generated {len(items)} recording upload URL(s), "
f"workflow {request.workflow_id}, org {user.selected_organization_id}"
)
return RecordingUploadResponseSchema(
upload_url=upload_url,
recording_id=recording_id,
storage_key=storage_key,
)
return BatchRecordingUploadResponseSchema(items=items)
except HTTPException:
raise
except Exception as exc:
logger.error(f"Error generating recording upload URL: {exc}")
logger.error(f"Error generating recording upload URLs: {exc}")
raise HTTPException(
status_code=500, detail="Failed to generate upload URL"
status_code=500, detail="Failed to generate upload URLs"
) from exc
@router.post(
"/",
response_model=RecordingResponseSchema,
summary="Create recording record after upload",
response_model=BatchRecordingCreateResponseSchema,
summary="Create recording records after upload",
)
async def create_recording(
request: RecordingCreateRequestSchema,
async def create_recordings(
request: BatchRecordingCreateRequestSchema,
user=Depends(get_user),
):
"""Create a recording record after the audio has been uploaded to storage."""
"""Create one or more recording records after audio files have been uploaded."""
try:
backend = StorageBackend.get_current_backend()
results = []
recording = await db_client.create_recording(
recording_id=request.recording_id,
workflow_id=request.workflow_id,
organization_id=user.selected_organization_id,
tts_provider=request.tts_provider,
tts_model=request.tts_model,
tts_voice_id=request.tts_voice_id,
transcript=request.transcript,
storage_key=request.storage_key,
storage_backend=backend.value,
created_by=user.id,
metadata=request.metadata,
)
for rec_req in request.recordings:
recording = await db_client.create_recording(
recording_id=rec_req.recording_id,
workflow_id=rec_req.workflow_id,
organization_id=user.selected_organization_id,
tts_provider=rec_req.tts_provider,
tts_model=rec_req.tts_model,
tts_voice_id=rec_req.tts_voice_id,
transcript=rec_req.transcript,
storage_key=rec_req.storage_key,
storage_backend=backend.value,
created_by=user.id,
metadata=rec_req.metadata,
)
results.append(_build_response(recording))
logger.info(
f"Created recording {request.recording_id} for workflow {request.workflow_id}"
f"Created {len(results)} recording(s) for "
f"workflow {request.recordings[0].workflow_id}"
)
return _build_response(recording)
return BatchRecordingCreateResponseSchema(recordings=results)
except HTTPException:
raise
except Exception as exc:
logger.error(f"Error creating recording: {exc}")
logger.error(f"Error creating recordings: {exc}")
raise HTTPException(
status_code=500, detail="Failed to create recording"
status_code=500, detail="Failed to create recordings"
) from exc

View file

@ -6,10 +6,17 @@ from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
class RecordingUploadRequestSchema(BaseModel):
"""Request schema for getting a presigned upload URL."""
class RecordingUploadResponseSchema(BaseModel):
"""Response schema with presigned upload URL."""
upload_url: str = Field(..., description="Presigned URL for uploading the audio")
recording_id: str = Field(..., description="Short unique recording ID")
storage_key: str = Field(..., description="Storage key where file will be uploaded")
class FileDescriptor(BaseModel):
"""Descriptor for a single file in a batch upload request."""
workflow_id: int = Field(..., description="Workflow ID this recording belongs to")
filename: str = Field(..., description="Original filename of the audio file")
mime_type: str = Field(
default="audio/wav", description="MIME type of the audio file"
@ -22,12 +29,21 @@ class RecordingUploadRequestSchema(BaseModel):
)
class RecordingUploadResponseSchema(BaseModel):
"""Response schema with presigned upload URL."""
class BatchRecordingUploadRequestSchema(BaseModel):
"""Request schema for getting presigned upload URLs for one or more files."""
upload_url: str = Field(..., description="Presigned URL for uploading the audio")
recording_id: str = Field(..., description="Short unique recording ID")
storage_key: str = Field(..., description="Storage key where file will be uploaded")
workflow_id: int = Field(..., description="Workflow ID these recordings belong to")
files: List[FileDescriptor] = Field(
..., min_length=1, max_length=20, description="List of files to upload"
)
class BatchRecordingUploadResponseSchema(BaseModel):
"""Response schema with presigned upload URLs."""
items: List[RecordingUploadResponseSchema] = Field(
..., description="Upload URLs for each file"
)
class RecordingCreateRequestSchema(BaseModel):
@ -66,6 +82,22 @@ class RecordingResponseSchema(BaseModel):
is_active: bool
class BatchRecordingCreateRequestSchema(BaseModel):
"""Request schema for creating one or more recording records after upload."""
recordings: List[RecordingCreateRequestSchema] = Field(
..., min_length=1, max_length=20, description="List of recordings to create"
)
class BatchRecordingCreateResponseSchema(BaseModel):
"""Response schema for recording creation."""
recordings: List[RecordingResponseSchema] = Field(
..., description="Created recording records"
)
class RecordingListResponseSchema(BaseModel):
"""Response schema for list of recordings."""

4727
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@hey-api/client-fetch": "^0.10.0",
"@nangohq/frontend": "^0.60.3",
"@nangohq/frontend": "^0.69.47",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.12",
@ -30,7 +30,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@sentry/nextjs": "^9.28.1",
"@stackframe/stack": "^2.8.52",
"@stackframe/stack": "^2.8.80",
"@xyflow/react": "^12.9.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -59,13 +59,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@hey-api/openapi-ts": "^0.66.2",
"cross-env": "^7.0.3",
"@hey-api/openapi-ts": "^0.95.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/source-map-support": "^0.5.10",
"cross-env": "^7.0.3",
"eslint": "^9",
"eslint-config-next": "^15.3.3",
"eslint-plugin-simple-import-sort": "^12.1.1",

View file

@ -1,11 +1,11 @@
import { Loader2, Mic, Pause, Play, Square, Trash2Icon, Upload } from "lucide-react";
import { Loader2, Mic, Pause, Play, Square, Trash2Icon, Upload, X } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
createRecordingApiV1WorkflowRecordingsPost,
createRecordingsApiV1WorkflowRecordingsPost,
deleteRecordingApiV1WorkflowRecordingsRecordingIdDelete,
getSignedUrlApiV1S3SignedUrlGet,
getUploadUrlApiV1WorkflowRecordingsUploadUrlPost,
getUploadUrlsApiV1WorkflowRecordingsUploadUrlPost,
listRecordingsApiV1WorkflowRecordingsGet,
transcribeAudioApiV1WorkflowRecordingsTranscribePost,
} from "@/client";
@ -40,7 +40,17 @@ interface RecordingsDialogProps {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
type RecordingStep = "idle" | "naming" | "recording" | "transcribing";
type RecordingStep = "idle" | "naming" | "recording";
interface PendingFile {
id: string;
file: File;
transcript: string;
isTranscribing: boolean;
error?: string;
}
let pendingFileCounter = 0;
export const RecordingsDialog = ({
open,
@ -52,8 +62,7 @@ export const RecordingsDialog = ({
const [recordings, setRecordings] = useState<RecordingResponseSchema[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [transcript, setTranscript] = useState("");
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
const [error, setError] = useState<string | null>(null);
const [language, setLanguage] = useState("multi");
const [recordingStep, setRecordingStep] = useState<RecordingStep>("idle");
@ -125,8 +134,7 @@ export const RecordingsDialog = ({
if (open) {
fetchRecordings();
setError(null);
setTranscript("");
setSelectedFile(null);
setPendingFiles([]);
setLanguage("multi");
resetRecordingState();
}
@ -140,8 +148,10 @@ export const RecordingsDialog = ({
}
}, [open, stopRecording, stopRecordingTimer, stopPlayback]);
const transcribeFile = async (file: File) => {
setRecordingStep("transcribing");
const transcribeFile = async (pendingId: string, file: File) => {
setPendingFiles((prev) =>
prev.map((p) => (p.id === pendingId ? { ...p, isTranscribing: true } : p))
);
try {
const currentLang = languageRef.current;
const result = await transcribeAudioApiV1WorkflowRecordingsTranscribePost({
@ -149,16 +159,55 @@ export const RecordingsDialog = ({
});
const data = result.data as Record<string, unknown> | undefined;
if (data?.transcript) {
setTranscript(data.transcript as string);
setPendingFiles((prev) =>
prev.map((p) =>
p.id === pendingId ? { ...p, transcript: data.transcript as string, isTranscribing: false } : p
)
);
} else {
setPendingFiles((prev) =>
prev.map((p) => (p.id === pendingId ? { ...p, isTranscribing: false } : p))
);
}
} catch {
// Transcription failed — user can still type manually
setError("Auto-transcription failed. You can type the transcript manually.");
} finally {
setRecordingStep("idle");
setPendingFiles((prev) =>
prev.map((p) =>
p.id === pendingId
? { ...p, isTranscribing: false, error: "Auto-transcription failed" }
: p
)
);
}
};
const addPendingFiles = (files: File[]) => {
const valid: PendingFile[] = [];
for (const file of files) {
if (file.size > MAX_FILE_SIZE) {
setError(`${file.name} (${(file.size / (1024 * 1024)).toFixed(1)}MB) exceeds 5MB limit — skipped.`);
continue;
}
const id = `pending-${++pendingFileCounter}`;
valid.push({ id, file, transcript: "", isTranscribing: false });
}
if (valid.length === 0) return;
setPendingFiles((prev) => [...prev, ...valid]);
setError(null);
for (const pf of valid) {
transcribeFile(pf.id, pf.file);
}
};
const removePendingFile = (pendingId: string) => {
setPendingFiles((prev) => prev.filter((p) => p.id !== pendingId));
};
const updateTranscript = (pendingId: string, transcript: string) => {
setPendingFiles((prev) =>
prev.map((p) => (p.id === pendingId ? { ...p, transcript } : p))
);
};
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@ -183,9 +232,8 @@ export const RecordingsDialog = ({
}
const ext = mediaRecorder.mimeType.includes("webm") ? "webm" : "mp4";
const file = new File([blob], `${filename}.${ext}`, { type: mediaRecorder.mimeType });
setSelectedFile(file);
setError(null);
transcribeFile(file);
resetRecordingState();
addPendingFiles([file]);
};
mediaRecorder.start();
@ -205,20 +253,15 @@ export const RecordingsDialog = ({
stopRecording();
};
const handleFileSelect = (file: File | null) => {
if (file && file.size > MAX_FILE_SIZE) {
setError(`File size (${(file.size / (1024 * 1024)).toFixed(1)}MB) exceeds the maximum allowed size of 5MB.`);
setSelectedFile(null);
if (fileInputRef.current) fileInputRef.current.value = "";
return;
}
setError(null);
setSelectedFile(file);
if (file) transcribeFile(file);
const handleFileSelect = (fileList: FileList | null) => {
if (!fileList || fileList.length === 0) return;
addPendingFiles(Array.from(fileList));
if (fileInputRef.current) fileInputRef.current.value = "";
};
const handleUpload = async () => {
if (!selectedFile || !transcript.trim()) return;
const ready = pendingFiles.filter((p) => p.transcript.trim() && !p.isTranscribing);
if (ready.length === 0) return;
if (!ttsProvider || !ttsModel || !ttsVoiceId) {
setError(
"TTS configuration (provider, model, voice) must be set in your user configuration before uploading."
@ -230,66 +273,72 @@ export const RecordingsDialog = ({
setError(null);
try {
// Step 1: Get presigned URL
// Step 1: Get presigned URLs for all files
const uploadUrlResponse =
await getUploadUrlApiV1WorkflowRecordingsUploadUrlPost({
await getUploadUrlsApiV1WorkflowRecordingsUploadUrlPost({
body: {
workflow_id: workflowId,
filename: selectedFile.name,
mime_type: selectedFile.type || "audio/wav",
file_size: selectedFile.size,
files: ready.map((p) => ({
filename: p.file.name,
mime_type: p.file.type || "audio/wav",
file_size: p.file.size,
})),
},
});
if (!uploadUrlResponse.data) {
throw new Error("Failed to get upload URL");
if (!uploadUrlResponse.data?.items) {
throw new Error("Failed to get upload URLs");
}
const { upload_url, recording_id, storage_key } =
uploadUrlResponse.data;
const items = uploadUrlResponse.data.items;
// Step 2: Upload file directly to storage
const uploadResponse = await fetch(upload_url, {
method: "PUT",
body: selectedFile,
headers: {
"Content-Type": selectedFile.type || "audio/wav",
},
});
// Step 2: Upload all files to storage in parallel
await Promise.all(
items.map(async (item, idx) => {
const file = ready[idx].file;
const uploadResponse = await fetch(item.upload_url, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type || "audio/wav",
},
});
if (!uploadResponse.ok) {
throw new Error(`File upload failed for ${file.name}`);
}
})
);
if (!uploadResponse.ok) {
throw new Error("File upload failed");
}
// Step 3: Create recording record
await createRecordingApiV1WorkflowRecordingsPost({
// Step 3: Create all recording records
await createRecordingsApiV1WorkflowRecordingsPost({
body: {
recording_id,
workflow_id: workflowId,
tts_provider: ttsProvider,
tts_model: ttsModel,
tts_voice_id: ttsVoiceId,
transcript: transcript.trim(),
storage_key,
metadata: {
original_filename: selectedFile.name,
file_size_bytes: selectedFile.size,
mime_type: selectedFile.type,
language,
},
recordings: items.map((item, idx) => ({
recording_id: item.recording_id,
workflow_id: workflowId,
tts_provider: ttsProvider,
tts_model: ttsModel,
tts_voice_id: ttsVoiceId,
transcript: ready[idx].transcript.trim(),
storage_key: item.storage_key,
metadata: {
original_filename: ready[idx].file.name,
file_size_bytes: ready[idx].file.size,
mime_type: ready[idx].file.type,
language,
},
})),
},
});
// Reset form and refresh list
setTranscript("");
setSelectedFile(null);
setPendingFiles([]);
setLanguage("multi");
resetRecordingState();
if (fileInputRef.current) fileInputRef.current.value = "";
await fetchRecordings();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to upload recording"
err instanceof Error ? err.message : "Failed to upload recordings"
);
} finally {
setUploading(false);
@ -335,8 +384,9 @@ export const RecordingsDialog = ({
};
const isRecording = recordingStep === "recording";
const isTranscribing = recordingStep === "transcribing";
const isBusy = uploading || isRecording || isTranscribing;
const anyTranscribing = pendingFiles.some((p) => p.isTranscribing);
const readyCount = pendingFiles.filter((p) => p.transcript.trim() && !p.isTranscribing).length;
const isBusy = uploading || isRecording || anyTranscribing;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -383,19 +433,20 @@ export const RecordingsDialog = ({
{/* Upload Section */}
<div className="space-y-3 border rounded-md p-3">
<Label className="text-sm font-medium">Add New Recording</Label>
<Label className="text-sm font-medium">Add New Recordings</Label>
{/* Audio source: file picker or record */}
<div>
<Label className="text-xs text-muted-foreground">
Audio File
Audio Files
</Label>
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={(e) => handleFileSelect(e.target.files?.[0] ?? null)}
multiple
onChange={(e) => handleFileSelect(e.target.files)}
className="hidden"
/>
<Button
@ -407,13 +458,7 @@ export const RecordingsDialog = ({
disabled={isBusy}
>
<Upload className="w-4 h-4 mr-2 shrink-0" />
{selectedFile && recordingStep !== "naming" ? (
<span className="truncate">
{selectedFile.name} ({(selectedFile.size / (1024 * 1024)).toFixed(1)}MB)
</span>
) : (
<span className="text-muted-foreground">Choose audio file (max 5MB)</span>
)}
<span className="text-muted-foreground">Choose audio files (max 5MB each)</span>
</Button>
{recordingStep === "idle" && (
<Button
@ -421,7 +466,7 @@ export const RecordingsDialog = ({
variant="outline"
size="sm"
onClick={() => setRecordingStep("naming")}
disabled={uploading || isTranscribing}
disabled={uploading || anyTranscribing}
>
<Mic className="w-4 h-4 mr-1" />
Record
@ -489,11 +534,44 @@ export const RecordingsDialog = ({
</div>
)}
{/* Transcribing progress */}
{isTranscribing && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="w-4 h-4 animate-spin" />
Transcribing audio...
{/* Pending files list */}
{pendingFiles.length > 0 && (
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Pending ({pendingFiles.length} file{pendingFiles.length !== 1 ? "s" : ""})
</Label>
{pendingFiles.map((pf) => (
<div key={pf.id} className="rounded-md border p-2 space-y-1.5 bg-muted/10">
<div className="flex items-center gap-2">
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono truncate flex-1">
{pf.file.name} ({(pf.file.size / (1024 * 1024)).toFixed(1)}MB)
</code>
{pf.isTranscribing && (
<Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground shrink-0" />
)}
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 shrink-0"
onClick={() => removePendingFile(pf.id)}
disabled={uploading}
>
<X className="w-3.5 h-3.5" />
</Button>
</div>
{pf.error && (
<p className="text-xs text-destructive">{pf.error}</p>
)}
<Textarea
placeholder={pf.isTranscribing ? "Transcribing..." : "What does this recording say?"}
value={pf.transcript}
onChange={(e) => updateTranscript(pf.id, e.target.value)}
disabled={pf.isTranscribing}
rows={2}
className="resize-none text-sm"
/>
</div>
))}
</div>
)}
@ -516,32 +594,19 @@ export const RecordingsDialog = ({
</Select>
</div>
{/* Transcript */}
<div>
<Label className="text-xs text-muted-foreground">
Transcript
</Label>
<Textarea
placeholder={isTranscribing ? "Transcribing..." : "What does this recording say?"}
value={transcript}
onChange={(e) => setTranscript(e.target.value)}
disabled={isTranscribing}
rows={3}
className="resize-none text-sm"
/>
</div>
<Button
size="sm"
onClick={handleUpload}
disabled={!selectedFile || !transcript.trim() || isBusy}
disabled={readyCount === 0 || isBusy}
>
{uploading ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<Upload className="w-4 h-4 mr-1" />
)}
{uploading ? "Uploading..." : "Upload Recording"}
{uploading
? "Uploading..."
: `Upload ${readyCount} Recording${readyCount !== 1 ? "s" : ""}`}
</Button>
</div>

File diff suppressed because one or more lines are too long

View file

@ -80,6 +80,50 @@ export type AuthUserResponse = {
is_superuser: boolean;
};
/**
* Request schema for creating one or more recording records after upload.
*/
export type BatchRecordingCreateRequestSchema = {
/**
* List of recordings to create
*/
recordings: Array<RecordingCreateRequestSchema>;
};
/**
* Response schema for recording creation.
*/
export type BatchRecordingCreateResponseSchema = {
/**
* Created recording records
*/
recordings: Array<RecordingResponseSchema>;
};
/**
* Request schema for getting presigned upload URLs for one or more files.
*/
export type BatchRecordingUploadRequestSchema = {
/**
* Workflow ID these recordings belong to
*/
workflow_id: number;
/**
* List of files to upload
*/
files: Array<FileDescriptor>;
};
/**
* Response schema with presigned upload URLs.
*/
export type BatchRecordingUploadResponseSchema = {
/**
* Upload URLs for each file
*/
items: Array<RecordingUploadResponseSchema>;
};
export type BodyTranscribeAudioApiV1WorkflowRecordingsTranscribePost = {
file: Blob | File;
language?: string;
@ -634,6 +678,24 @@ export type EndCallToolDefinition = {
config: EndCallConfig;
};
/**
* Descriptor for a single file in a batch upload request.
*/
export type FileDescriptor = {
/**
* Original filename of the audio file
*/
filename: string;
/**
* MIME type of the audio file
*/
mime_type?: string;
/**
* File size in bytes (max 5MB)
*/
file_size: number;
};
export type FileMetadataResponse = {
key: string;
metadata: {
@ -909,28 +971,6 @@ export type RecordingResponseSchema = {
is_active: boolean;
};
/**
* Request schema for getting a presigned upload URL.
*/
export type RecordingUploadRequestSchema = {
/**
* Workflow ID this recording belongs to
*/
workflow_id: number;
/**
* Original filename of the audio file
*/
filename: string;
/**
* MIME type of the audio file
*/
mime_type?: string;
/**
* File size in bytes (max 5MB)
*/
file_size: number;
};
/**
* Response schema with presigned upload URL.
*/
@ -5464,8 +5504,8 @@ export type SearchChunksApiV1KnowledgeBaseSearchPostResponses = {
export type SearchChunksApiV1KnowledgeBaseSearchPostResponse = SearchChunksApiV1KnowledgeBaseSearchPostResponses[keyof SearchChunksApiV1KnowledgeBaseSearchPostResponses];
export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostData = {
body: RecordingUploadRequestSchema;
export type GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostData = {
body: BatchRecordingUploadRequestSchema;
headers?: {
authorization?: string | null;
'X-API-Key'?: string | null;
@ -5475,7 +5515,7 @@ export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostData = {
url: '/api/v1/workflow-recordings/upload-url';
};
export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostErrors = {
export type GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostErrors = {
/**
* Not found
*/
@ -5486,16 +5526,16 @@ export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostErrors = {
422: HttpValidationError;
};
export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostError = GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostErrors[keyof GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostErrors];
export type GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostError = GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostErrors[keyof GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostErrors];
export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponses = {
export type GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostResponses = {
/**
* Successful Response
*/
200: RecordingUploadResponseSchema;
200: BatchRecordingUploadResponseSchema;
};
export type GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponse = GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponses[keyof GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponses];
export type GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostResponse = GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostResponses[keyof GetUploadUrlsApiV1WorkflowRecordingsUploadUrlPostResponses];
export type ListRecordingsApiV1WorkflowRecordingsGetData = {
body?: never;
@ -5547,8 +5587,8 @@ export type ListRecordingsApiV1WorkflowRecordingsGetResponses = {
export type ListRecordingsApiV1WorkflowRecordingsGetResponse = ListRecordingsApiV1WorkflowRecordingsGetResponses[keyof ListRecordingsApiV1WorkflowRecordingsGetResponses];
export type CreateRecordingApiV1WorkflowRecordingsPostData = {
body: RecordingCreateRequestSchema;
export type CreateRecordingsApiV1WorkflowRecordingsPostData = {
body: BatchRecordingCreateRequestSchema;
headers?: {
authorization?: string | null;
'X-API-Key'?: string | null;
@ -5558,7 +5598,7 @@ export type CreateRecordingApiV1WorkflowRecordingsPostData = {
url: '/api/v1/workflow-recordings/';
};
export type CreateRecordingApiV1WorkflowRecordingsPostErrors = {
export type CreateRecordingsApiV1WorkflowRecordingsPostErrors = {
/**
* Not found
*/
@ -5569,16 +5609,16 @@ export type CreateRecordingApiV1WorkflowRecordingsPostErrors = {
422: HttpValidationError;
};
export type CreateRecordingApiV1WorkflowRecordingsPostError = CreateRecordingApiV1WorkflowRecordingsPostErrors[keyof CreateRecordingApiV1WorkflowRecordingsPostErrors];
export type CreateRecordingsApiV1WorkflowRecordingsPostError = CreateRecordingsApiV1WorkflowRecordingsPostErrors[keyof CreateRecordingsApiV1WorkflowRecordingsPostErrors];
export type CreateRecordingApiV1WorkflowRecordingsPostResponses = {
export type CreateRecordingsApiV1WorkflowRecordingsPostResponses = {
/**
* Successful Response
*/
200: RecordingResponseSchema;
200: BatchRecordingCreateResponseSchema;
};
export type CreateRecordingApiV1WorkflowRecordingsPostResponse = CreateRecordingApiV1WorkflowRecordingsPostResponses[keyof CreateRecordingApiV1WorkflowRecordingsPostResponses];
export type CreateRecordingsApiV1WorkflowRecordingsPostResponse = CreateRecordingsApiV1WorkflowRecordingsPostResponses[keyof CreateRecordingsApiV1WorkflowRecordingsPostResponses];
export type DeleteRecordingApiV1WorkflowRecordingsRecordingIdDeleteData = {
body?: never;