Spaces:
Running
Running
| import { RemoteCompute } from "./RemoteCompute.svelte"; | |
| import type { Position3D } from "$lib/types/positionable.js"; | |
| import { generateName } from "$lib/utils/generateName.js"; | |
| import { positionManager } from "$lib/utils/positionManager.js"; | |
| import { | |
| rootGet, | |
| healthCheckHealthGet, | |
| listSessionsSessionsGet, | |
| createSessionSessionsPost, | |
| startInferenceSessionsSessionIdStartPost, | |
| stopInferenceSessionsSessionIdStopPost, | |
| deleteSessionSessionsSessionIdDelete | |
| } from "@robothub/inference-server-client"; | |
| import { settings } from "$lib/runes/settings.svelte"; | |
| import type { | |
| CreateSessionRequest, | |
| CreateSessionResponse | |
| } from "@robothub/inference-server-client"; | |
| export type ModelType = "act" | "diffusion" | "smolvla" | "pi0" | "groot" | "custom"; | |
| export interface ModelTypeConfig { | |
| id: ModelType; | |
| label: string; | |
| icon: string; | |
| description: string; | |
| defaultPolicyPath: string; | |
| defaultCameraNames: string[]; | |
| requiresLanguageInstruction?: boolean; | |
| enabled: boolean; | |
| } | |
| export const MODEL_TYPES: Record<ModelType, ModelTypeConfig> = { | |
| act: { | |
| id: "act", | |
| label: "ACT Model", | |
| icon: "icon-[mdi--brain]", | |
| description: "Action Chunking with Transformers", | |
| defaultPolicyPath: "LaetusH/act_so101_beyond", | |
| defaultCameraNames: ["front"], | |
| enabled: true | |
| }, | |
| diffusion: { | |
| id: "diffusion", | |
| label: "Diffusion Policy", | |
| icon: "icon-[mdi--creation]", | |
| description: "Diffusion-based robot control", | |
| defaultPolicyPath: "diffusion_policy/default", | |
| defaultCameraNames: ["front", "wrist"], | |
| enabled: true | |
| }, | |
| smolvla: { | |
| id: "smolvla", | |
| label: "SmolVLA", | |
| icon: "icon-[mdi--eye-outline]", | |
| description: "Small Vision-Language-Action model", | |
| defaultPolicyPath: "smolvla/latest", | |
| defaultCameraNames: ["front"], | |
| requiresLanguageInstruction: true, | |
| enabled: true | |
| }, | |
| pi0: { | |
| id: "pi0", | |
| label: "Pi0", | |
| icon: "icon-[mdi--pi]", | |
| description: "Lightweight robotics model", | |
| defaultPolicyPath: "pi0/base", | |
| defaultCameraNames: ["front"], | |
| enabled: true | |
| }, | |
| groot: { | |
| id: "groot", | |
| label: "NVIDIA Groot", | |
| icon: "icon-[mdi--robot-outline]", | |
| description: "Humanoid robotics foundation model", | |
| defaultPolicyPath: "nvidia/groot", | |
| defaultCameraNames: ["front", "left", "right"], | |
| requiresLanguageInstruction: true, | |
| enabled: false // Not yet implemented | |
| }, | |
| custom: { | |
| id: "custom", | |
| label: "Custom Model", | |
| icon: "icon-[mdi--cog]", | |
| description: "Custom model configuration", | |
| defaultPolicyPath: "", | |
| defaultCameraNames: ["front"], | |
| enabled: true | |
| } | |
| }; | |
| export interface AISessionConfig { | |
| sessionId: string; | |
| modelType: ModelType; | |
| policyPath: string; | |
| cameraNames: string[]; | |
| transportServerUrl: string; | |
| workspaceId?: string; | |
| languageInstruction?: string; | |
| } | |
| export interface AISessionResponse { | |
| workspace_id: string; | |
| camera_room_ids: Record<string, string>; | |
| joint_input_room_id: string; | |
| joint_output_room_id: string; | |
| } | |
| export interface AISessionStatus { | |
| session_id: string; | |
| status: "initializing" | "ready" | "running" | "stopped"; | |
| policy_path: string; | |
| camera_names: string[]; | |
| workspace_id: string; | |
| rooms: { | |
| workspace_id: string; | |
| camera_room_ids: Record<string, string>; | |
| joint_input_room_id: string; | |
| joint_output_room_id: string; | |
| }; | |
| stats: { | |
| inference_count: number; | |
| commands_sent: number; | |
| joints_received: number; | |
| images_received: Record<string, number>; | |
| errors: number; | |
| actions_in_queue: number; | |
| }; | |
| inference_stats?: { | |
| inference_count: number; | |
| total_inference_time: number; | |
| average_inference_time: number; | |
| average_fps: number; | |
| is_loaded: boolean; | |
| device: string; | |
| }; | |
| error_message?: string; | |
| } | |
| export class RemoteComputeManager { | |
| private _computes = $state<RemoteCompute[]>([]); | |
| constructor() { | |
| // No client initialization needed anymore | |
| } | |
| // Reactive getters | |
| get computes(): RemoteCompute[] { | |
| return this._computes; | |
| } | |
| get computeCount(): number { | |
| return this._computes.length; | |
| } | |
| get runningComputes(): RemoteCompute[] { | |
| return this._computes.filter((compute) => compute.status === "running"); | |
| } | |
| /** | |
| * Get available model types | |
| */ | |
| get availableModelTypes(): ModelTypeConfig[] { | |
| return Object.values(MODEL_TYPES).filter((model) => model.enabled); | |
| } | |
| /** | |
| * Get model type configuration | |
| */ | |
| getModelTypeConfig(modelType: ModelType): ModelTypeConfig | undefined { | |
| return MODEL_TYPES[modelType]; | |
| } | |
| /** | |
| * Create a new AI compute instance with full configuration | |
| */ | |
| async createComputeWithSession( | |
| config: AISessionConfig, | |
| computeId?: string, | |
| computeName?: string, | |
| position?: Position3D | |
| ): Promise<{ success: boolean; error?: string; compute?: RemoteCompute }> { | |
| const finalComputeId = computeId || generateName(); | |
| // Check if compute already exists | |
| if (this._computes.find((c) => c.id === finalComputeId)) { | |
| return { success: false, error: `Compute with ID ${finalComputeId} already exists` }; | |
| } | |
| try { | |
| // Create compute instance | |
| const compute = new RemoteCompute(finalComputeId, computeName); | |
| compute.modelType = config.modelType; | |
| // Set position (from position manager if not provided) | |
| compute.position = position || positionManager.getNextPosition(); | |
| // Add to reactive array | |
| this._computes.push(compute); | |
| // Create the session immediately | |
| const sessionResult = await this.createSession(compute.id, config); | |
| if (!sessionResult.success) { | |
| // Remove compute if session creation failed | |
| await this.removeCompute(compute.id); | |
| return { success: false, error: sessionResult.error }; | |
| } | |
| console.log( | |
| `Created compute ${finalComputeId} with ${config.modelType} model at position (${compute.position.x.toFixed(1)}, ${compute.position.y.toFixed(1)}, ${compute.position.z.toFixed(1)}). Total computes: ${this._computes.length}` | |
| ); | |
| return { success: true, compute }; | |
| } catch (error) { | |
| console.error("Failed to create compute with session:", error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Create a new AI compute instance (legacy method) | |
| */ | |
| createCompute(id?: string, name?: string, position?: Position3D): RemoteCompute { | |
| const computeId = id || generateName(); | |
| // Check if compute already exists | |
| if (this._computes.find((c) => c.id === computeId)) { | |
| throw new Error(`Compute with ID ${computeId} already exists`); | |
| } | |
| // Create compute instance | |
| const compute = new RemoteCompute(computeId, name); | |
| // Set position (from position manager if not provided) | |
| compute.position = position || positionManager.getNextPosition(); | |
| // Add to reactive array | |
| this._computes.push(compute); | |
| console.log( | |
| `Created compute ${computeId} at position (${compute.position.x.toFixed(1)}, ${compute.position.y.toFixed(1)}, ${compute.position.z.toFixed(1)}). Total computes: ${this._computes.length}` | |
| ); | |
| return compute; | |
| } | |
| /** | |
| * Get compute by ID | |
| */ | |
| getCompute(id: string): RemoteCompute | undefined { | |
| return this._computes.find((c) => c.id === id); | |
| } | |
| /** | |
| * Remove a compute instance | |
| */ | |
| async removeCompute(id: string): Promise<void> { | |
| const computeIndex = this._computes.findIndex((c) => c.id === id); | |
| if (computeIndex === -1) return; | |
| const compute = this._computes[computeIndex]; | |
| // Clean up compute resources | |
| await this.stopSession(id); | |
| await this.deleteSession(id); | |
| // Remove from reactive array | |
| this._computes.splice(computeIndex, 1); | |
| console.log(`Removed compute ${id}. Remaining computes: ${this._computes.length}`); | |
| } | |
| /** | |
| * Create an Inference Session | |
| */ | |
| async createSession( | |
| computeId: string, | |
| config: AISessionConfig | |
| ): Promise<{ success: boolean; error?: string; data?: AISessionResponse }> { | |
| const compute = this.getCompute(computeId); | |
| if (!compute) { | |
| return { success: false, error: `Compute ${computeId} not found` }; | |
| } | |
| try { | |
| const request: CreateSessionRequest = { | |
| session_id: config.sessionId, | |
| policy_path: config.policyPath, | |
| camera_names: config.cameraNames, | |
| transport_server_url: config.transportServerUrl, | |
| workspace_id: config.workspaceId || undefined, | |
| policy_type: config.modelType, // Use model type as policy type | |
| language_instruction: config.languageInstruction || undefined | |
| }; | |
| const response = await createSessionSessionsPost({ | |
| body: request, | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| if (!response.data) { | |
| throw new Error("Failed to create session - no data returned"); | |
| } | |
| const data: CreateSessionResponse = response.data; | |
| // Update compute with session info | |
| compute.sessionId = config.sessionId; | |
| compute.status = "ready"; | |
| compute.sessionConfig = config; | |
| compute.sessionData = { | |
| workspace_id: data.workspace_id, | |
| camera_room_ids: data.camera_room_ids, | |
| joint_input_room_id: data.joint_input_room_id, | |
| joint_output_room_id: data.joint_output_room_id | |
| }; | |
| return { success: true, data: compute.sessionData }; | |
| } catch (error) { | |
| console.error(`Failed to create session for compute ${computeId}:`, error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Start inference for a session | |
| */ | |
| async startSession(computeId: string): Promise<{ success: boolean; error?: string }> { | |
| const compute = this.getCompute(computeId); | |
| if (!compute || !compute.sessionId) { | |
| return { success: false, error: "No session to start" }; | |
| } | |
| try { | |
| await startInferenceSessionsSessionIdStartPost({ | |
| path: { session_id: compute.sessionId }, | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| compute.status = "running"; | |
| return { success: true }; | |
| } catch (error) { | |
| console.error(`Failed to start session for compute ${computeId}:`, error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Stop inference for a session | |
| */ | |
| async stopSession(computeId: string): Promise<{ success: boolean; error?: string }> { | |
| const compute = this.getCompute(computeId); | |
| if (!compute || !compute.sessionId) { | |
| return { success: false, error: "No session to stop" }; | |
| } | |
| try { | |
| await stopInferenceSessionsSessionIdStopPost({ | |
| path: { session_id: compute.sessionId }, | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| compute.status = "stopped"; | |
| return { success: true }; | |
| } catch (error) { | |
| console.error(`Failed to stop session for compute ${computeId}:`, error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Delete a session | |
| */ | |
| async deleteSession(computeId: string): Promise<{ success: boolean; error?: string }> { | |
| const compute = this.getCompute(computeId); | |
| if (!compute || !compute.sessionId) { | |
| return { success: true }; // Already deleted | |
| } | |
| try { | |
| await deleteSessionSessionsSessionIdDelete({ | |
| path: { session_id: compute.sessionId }, | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| // Reset compute session info | |
| compute.sessionId = null; | |
| compute.status = "disconnected"; | |
| compute.sessionConfig = null; | |
| compute.sessionData = null; | |
| return { success: true }; | |
| } catch (error) { | |
| console.error(`Failed to delete session for compute ${computeId}:`, error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Get session status | |
| */ | |
| async getSessionStatus( | |
| computeId: string | |
| ): Promise<{ success: boolean; data?: AISessionStatus; error?: string }> { | |
| const compute = this.getCompute(computeId); | |
| if (!compute || !compute.sessionId) { | |
| return { success: false, error: "No session found" }; | |
| } | |
| try { | |
| // Get all sessions and find the one we want | |
| const response = await listSessionsSessionsGet({ | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| if (!response.data) { | |
| throw new Error("Failed to get sessions list"); | |
| } | |
| const session = response.data.find((s) => s.session_id === compute.sessionId); | |
| if (!session) { | |
| throw new Error(`Session ${compute.sessionId} not found`); | |
| } | |
| // Update compute status | |
| compute.status = session.status as "initializing" | "ready" | "running" | "stopped"; | |
| // Convert to AISessionStatus format | |
| const sessionStatus: AISessionStatus = { | |
| session_id: session.session_id, | |
| status: session.status as "initializing" | "ready" | "running" | "stopped", | |
| policy_path: session.policy_path, | |
| camera_names: session.camera_names, | |
| workspace_id: session.workspace_id, | |
| rooms: session.rooms as any, | |
| stats: session.stats as any, | |
| inference_stats: session.inference_stats as any, | |
| error_message: session.error_message || undefined | |
| }; | |
| return { success: true, data: sessionStatus }; | |
| } catch (error) { | |
| console.error(`Failed to get session status for compute ${computeId}:`, error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Check AI server health | |
| */ | |
| async checkServerHealth(): Promise<{ success: boolean; data?: any; error?: string }> { | |
| try { | |
| const healthResponse = await rootGet({ | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| if (!healthResponse.data) { | |
| return { success: false, error: "Server is not healthy" }; | |
| } | |
| // Get detailed health info | |
| const detailedHealthResponse = await healthCheckHealthGet({ | |
| baseUrl: settings.inferenceServerUrl | |
| }); | |
| return { success: true, data: detailedHealthResponse.data }; | |
| } catch (error) { | |
| console.error("Failed to check AI server health:", error); | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Clean up all computes | |
| */ | |
| async destroy(): Promise<void> { | |
| const cleanupPromises = this._computes.map(async (compute) => { | |
| await this.stopSession(compute.id); | |
| await this.deleteSession(compute.id); | |
| }); | |
| await Promise.allSettled(cleanupPromises); | |
| this._computes.length = 0; | |
| } | |
| } | |
| // Global compute manager instance | |
| export const remoteComputeManager = new RemoteComputeManager(); | |