| | |
| | | export interface Detection { |
| | | class_id: number; |
| | | class_name: string; |
| | | confidence: number; |
| | | bbox_xyxy: number[]; |
| | | } |
| | | |
| | | export interface DetectionImage { |
| | | file: string; |
| | | annotated_file: string; |
| | | width: number; |
| | | height: number; |
| | | detections: Detection[]; |
| | | } |
| | | |
| | | export interface DetectionRun { |
| | | detection_count: number; |
| | | processed_images: number; |
| | | elapsed_seconds: number; |
| | | device: string; |
| | | model: string; |
| | | confidence: number; |
| | | input_dir: string; |
| | | notes: string[]; |
| | | } |
| | | |
| | | export interface TrajectoryEvent { |
| | | event_id: string; |
| | | event_type: string; |
| | | track_ids: string[]; |
| | | start_time: string; |
| | | end_time: string; |
| | | duration_seconds: number; |
| | | } |
| | | |
| | | export interface TrajectoryRun { |
| | | case_count: number; |
| | | track_count: number; |
| | | event_count: number; |
| | | dropped_duplicate_observations: number; |
| | | elapsed_seconds: number; |
| | | device: string; |
| | | input: string; |
| | | thresholds_by_case: Record<string, Record<string, number>>; |
| | | } |
| | | |
| | | export interface TrajectorySummary { |
| | | track_id: string; |
| | | entity_type: string; |
| | | point_count: string; |
| | | distance_m: string; |
| | | average_speed_mps: string; |
| | | behavior_labels: string; |
| | | } |
| | | export interface Detection { class_id: number; class_name: string; confidence: number; bbox_xyxy: number[]; } |
| | | export interface DetectionImage { file: string; annotated_file: string; width: number; height: number; detections: Detection[]; } |
| | | export interface DetectionRun { detection_count: number; processed_images: number; elapsed_seconds: number; device: string; model: string; confidence: number; input_dir: string; notes: string[]; created_at?: string; } |
| | | export interface DetectionCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; createdAt: string; run: DetectionRun; images: DetectionImage[]; } |
| | | export interface TrajectoryEvent { event_id: string; event_type: string; track_ids: string[]; start_time: string; end_time: string; duration_seconds: number; } |
| | | export interface TrajectoryCaseRun { case_id: string; input_count: number; track_count: number; event_count: number; dropped_duplicate_observations: number; elapsed_seconds: number; device: string; input: string; thresholds: Record<string, number>; created_at?: string; } |
| | | export interface TrajectorySummary { track_id: string; entity_type: string; point_count: string; distance_m: string; average_speed_mps: string; behavior_labels: string; } |
| | | export interface TrajectoryCase { id: string; label: string; note: string; artifactRoot: string; showSpatialContext: boolean; showFlyableZones: boolean; createdAt: string; run: TrajectoryCaseRun; events: TrajectoryEvent[]; summary: TrajectorySummary[]; } |
| | | interface CaseDefinition { id: string; label: string; note: string; artifactRoot: string; createdAt: string; } |
| | | interface TrajectoryDefinition extends CaseDefinition { showSpatialContext: boolean; showFlyableZones: boolean; } |
| | | interface DetectionDefinition extends CaseDefinition { inputRoot: string; } |
| | | export interface UploadFilePayload { name: string; content: string; } |
| | | |
| | | export const artifactUrl = (path: string) => `/${path.replace(/\\/g, "/").split("/").map(encodeURIComponent).join("/")}`; |
| | | |
| | | async function getJson<T>(path: string): Promise<T> { |
| | | const response = await fetch(artifactUrl(path), { cache: "no-store" }); |
| | | if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`); |
| | | return response.json() as Promise<T>; |
| | | async function responseJson<T>(response: Response): Promise<T> { |
| | | const payload = await response.json().catch(() => ({})) as { error?: string } & T; |
| | | if (!response.ok) throw new Error(payload.error || `本地服务请求失败 (${response.status})`); |
| | | return payload; |
| | | } |
| | | async function getJson<T>(path: string): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { cache: "no-store" })); } |
| | | async function getText(path: string): Promise<string> { const response = await fetch(artifactUrl(path), { cache: "no-store" }); if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`); return response.text(); } |
| | | function parseCsv(text: string): TrajectorySummary[] { const lines = text.trim().split(/\r?\n/); const headers = lines.shift()?.replace(/^\uFEFF/, "").split(",") ?? []; return lines.filter(Boolean).map((line) => { const values = line.split(","); return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])) as unknown as TrajectorySummary; }); } |
| | | |
| | | export async function loadTrajectoryArtifacts(): Promise<Record<string, TrajectoryCase>> { |
| | | const { runs } = await getJson<{ runs: TrajectoryDefinition[] }>("api/trajectory/runs"); |
| | | const cases = await Promise.all(runs.map(async (definition) => { |
| | | const [run, eventPayload, summary] = await Promise.all([getJson<TrajectoryCaseRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ events: TrajectoryEvent[] }>(`${definition.artifactRoot}/events.json`), getText(`${definition.artifactRoot}/trajectory_summary.csv`)]); |
| | | return { ...definition, run, events: eventPayload.events, summary: parseCsv(summary) } satisfies TrajectoryCase; |
| | | })); |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | |
| | | async function getText(path: string): Promise<string> { |
| | | const response = await fetch(artifactUrl(path), { cache: "no-store" }); |
| | | if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`); |
| | | return response.text(); |
| | | export async function loadDetectionArtifacts(): Promise<Record<string, DetectionCase>> { |
| | | const { runs } = await getJson<{ runs: DetectionDefinition[] }>("api/object-detection/runs"); |
| | | const cases = await Promise.all(runs.map(async (definition) => { |
| | | const [run, payload] = await Promise.all([getJson<DetectionRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ images: DetectionImage[] }>(`${definition.artifactRoot}/detections.json`)]); |
| | | return { ...definition, run, images: payload.images } satisfies DetectionCase; |
| | | })); |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | |
| | | function parseCsv(text: string): TrajectorySummary[] { |
| | | const lines = text.trim().split(/\r?\n/); |
| | | const headers = lines.shift()?.replace(/^\uFEFF/, "").split(",") ?? []; |
| | | return lines.filter(Boolean).map((line) => { |
| | | const values = line.split(","); |
| | | return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])) as unknown as TrajectorySummary; |
| | | }); |
| | | } |
| | | |
| | | export async function loadDetectionArtifacts() { |
| | | const [run, payload] = await Promise.all([ |
| | | getJson<DetectionRun>("shared/outputs/01-object-detection/run_metadata.json"), |
| | | getJson<{ images: DetectionImage[] }>("shared/outputs/01-object-detection/detections.json") |
| | | ]); |
| | | return { run, images: payload.images }; |
| | | } |
| | | |
| | | export async function loadTrajectoryArtifacts() { |
| | | const [run, difficultEvents, difficultSummary, normalSummary] = await Promise.all([ |
| | | getJson<TrajectoryRun>("shared/outputs/15-trajectory-analysis/run_metadata.json"), |
| | | getJson<{ events: TrajectoryEvent[] }>("shared/outputs/15-trajectory-analysis/difficult/events.json"), |
| | | getText("shared/outputs/15-trajectory-analysis/difficult/trajectory_summary.csv"), |
| | | getText("shared/outputs/15-trajectory-analysis/normal/trajectory_summary.csv") |
| | | ]); |
| | | return { |
| | | run, |
| | | cases: { |
| | | difficult: { events: difficultEvents.events, summary: parseCsv(difficultSummary) }, |
| | | normal: { events: [], summary: parseCsv(normalSummary) } |
| | | } |
| | | }; |
| | | } |
| | | async function postRun<T>(path: string, body: object): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })); } |
| | | export async function createTrajectoryRun(files: { flight: UploadFilePayload; route: UploadFilePayload; restricted: UploadFilePayload; flyable?: UploadFilePayload }) { return postRun<{ run: TrajectoryDefinition }>("api/trajectory/runs", { files }); } |
| | | export async function createDetectionRun(images: UploadFilePayload[]) { return postRun<{ run: DetectionDefinition }>("api/object-detection/runs", { images }); } |
| | | export function readFileAsPayload(file: File): Promise<UploadFilePayload> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error(`无法读取 ${file.name}`)); reader.onload = () => { const value = String(reader.result ?? ""); resolve({ name: file.name, content: value.slice(value.indexOf(",") + 1) }); }; reader.readAsDataURL(file); }); } |