File size: 6,440 Bytes
e327f0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f297df3
 
 
 
 
e327f0d
 
f297df3
e327f0d
 
f297df3
e327f0d
 
 
f297df3
 
e327f0d
f297df3
 
 
 
 
 
 
e327f0d
 
 
 
 
f297df3
 
 
 
 
 
e327f0d
 
 
 
 
 
 
f297df3
 
 
 
 
 
 
 
 
e327f0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/**
 * Model selection state for the inspection pipeline.
 *
 * The header dropdown lets a demo user switch between the upstream
 * pre-trained YOLO weights and the custom weights we fine-tuned on
 * Turkish-market vehicles. The chosen model id is:
 *
 *   1. persisted in localStorage so it survives a hard reload, and
 *   2. injected as `?model=<id>` on every `POST /api/v1/inspect` call.
 *
 * Backend exposes `GET /api/v1/models` to enumerate the available models.
 * If that endpoint is not yet live (parallel backend work), we fall back
 * to a hardcoded list so the UI still works in dev.
 */
import axios from 'axios';
import { API_BASE_URL, getStoredAccessToken } from './api';

export type ModelKind = 'pretrained' | 'custom';

export interface ModelOption {
  id: string;
  label: string;
  kind: ModelKind;
  /** Optional secondary line (e.g. "YOLOv8-seg · 80 sınıf"). */
  description?: string;
  /** True when this is the recommended default. */
  recommended?: boolean;
}

const STORAGE_KEY = 'selected_model';

/**
 * Hardcoded fallback used when `/api/v1/models` is unreachable. The first
 * entry of `kind: 'custom'` is the documented MVP default and gets
 * `recommended: true` so the UI badges it.
 */
// IMPORTANT: id'ler backend kontratiyla esleşmek zorunda. Backend
// services/backend/ml_service.py DEFAULT_MODEL_ID="custom" ve registry
// "pretrained_ultralytics_yolo11m" / "pretrained_roboflow_cardd" id'lerini
// taniyor. Yanlis id 400 "Bilinmeyen model" donduruyor → frontend
// "sunucu hatasi" gosteriyor.
export const FALLBACK_MODELS: ModelOption[] = [
  {
    id: 'custom',
    label: 'Kendi Modelim',
    kind: 'custom',
    description: 'YOLO11-seg · Bizim 3-model pipeline (damage + parts + severity)',
    recommended: true,
  },
  {
    id: 'pretrained_ultralytics_yolo11m',
    label: 'Pre-trained: Ultralytics',
    kind: 'pretrained',
    description: 'YOLO11m-seg · COCO 80-class baseline',
  },
  {
    id: 'pretrained_roboflow_cardd',
    label: 'Pre-trained: Roboflow Scratch/Dent',
    kind: 'pretrained',
    description: 'Roboflow Hosted Inference — sadece bbox tespiti',
  },
];

export const DEFAULT_MODEL_ID = FALLBACK_MODELS[0]!.id;

// Eski sürümlerde kaydedilmiş stale id'leri yeni kontratla map et.
const LEGACY_ID_MIGRATIONS: Record<string, string> = {
  'custom-yolo26-seg': 'custom',
  'pretrained-yolo26': 'pretrained_ultralytics_yolo11m',
};

function isBrowser(): boolean {
  return typeof window !== 'undefined';
}

export function getSelectedModelId(): string {
  if (!isBrowser()) return DEFAULT_MODEL_ID;
  try {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (!stored) return DEFAULT_MODEL_ID;
    // Migration: eski id varsa yeniyle güncelle
    if (LEGACY_ID_MIGRATIONS[stored]) {
      const newId = LEGACY_ID_MIGRATIONS[stored];
      localStorage.setItem(STORAGE_KEY, newId);
      return newId;
    }
    return stored;
  } catch {
    return DEFAULT_MODEL_ID;
  }
}

export function setSelectedModelId(id: string): void {
  if (!isBrowser()) return;
  try {
    localStorage.setItem(STORAGE_KEY, id);
    // Broadcast to other listeners in the same tab. The native `storage`
    // event only fires across tabs.
    window.dispatchEvent(new CustomEvent('selected_model_change', { detail: id }));
  } catch {
    /* localStorage unavailable: silently degrade */
  }
}

/**
 * Best-effort fetch of the available models. Never throws — on any
 * failure (network, 401, 404, malformed payload) we return the hardcoded
 * fallback so the dropdown is never empty.
 */
export async function fetchAvailableModels(opts: {
  signal?: AbortSignal;
} = {}): Promise<ModelOption[]> {
  try {
    const token = getStoredAccessToken();
    const res = await axios.get<unknown>(`${API_BASE_URL}/api/v1/models`, {
      timeout: 6_000,
      signal: opts.signal,
      headers: token ? { Authorization: `Bearer ${token}` } : undefined,
    });
    const parsed = parseModelsPayload(res.data);
    if (parsed.length > 0) return parsed;
    return FALLBACK_MODELS;
  } catch {
    return FALLBACK_MODELS;
  }
}

/**
 * Tolerant parser — backend hasn't pinned the response shape yet, so we
 * accept any of:
 *   - { models: [...] }
 *   - [...]
 *   - { items: [...] }
 * with item shape { id, name?|label?, kind?, description?, recommended? }.
 */
function parseModelsPayload(data: unknown): ModelOption[] {
  if (!data) return [];
  let arr: unknown[] = [];
  if (Array.isArray(data)) {
    arr = data;
  } else if (typeof data === 'object') {
    const d = data as Record<string, unknown>;
    if (Array.isArray(d.models)) arr = d.models;
    else if (Array.isArray(d.items)) arr = d.items;
  }
  const out: ModelOption[] = [];
  for (const raw of arr) {
    if (!raw || typeof raw !== 'object') continue;
    const r = raw as Record<string, unknown>;
    const id = typeof r.id === 'string' ? r.id : null;
    if (!id) continue;
    const label =
      typeof r.label === 'string'
        ? r.label
        : typeof r.name === 'string'
          ? r.name
          : id;
    const kindRaw = typeof r.kind === 'string' ? r.kind.toLowerCase() : '';
    const idLower = id.toLowerCase();
    const kind: ModelKind =
      kindRaw === 'pretrained'
        ? 'pretrained'
        : kindRaw === 'custom'
          ? 'custom'
          : idLower.includes('pretrain') || idLower.includes('coco')
            ? 'pretrained'
            : 'custom';
    out.push({
      id,
      label,
      kind,
      description:
        typeof r.description === 'string' ? r.description : undefined,
      recommended: r.recommended === true,
    });
  }
  return out;
}

/**
 * Subscribe to selected-model changes (both same-tab CustomEvent and
 * cross-tab `storage` events). Returns an unsubscribe function.
 */
export function subscribeSelectedModel(cb: (id: string) => void): () => void {
  if (!isBrowser()) return () => {};
  const onCustom = (e: Event) => {
    const detail = (e as CustomEvent<string>).detail;
    if (typeof detail === 'string') cb(detail);
  };
  const onStorage = (e: StorageEvent) => {
    if (e.key === STORAGE_KEY && typeof e.newValue === 'string') {
      cb(e.newValue);
    }
  };
  window.addEventListener('selected_model_change', onCustom);
  window.addEventListener('storage', onStorage);
  return () => {
    window.removeEventListener('selected_model_change', onCustom);
    window.removeEventListener('storage', onStorage);
  };
}