File size: 10,852 Bytes
2a7eb0c
 
 
 
 
 
08818fa
 
2a7eb0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
08818fa
2a7eb0c
 
 
 
 
 
 
08818fa
 
2a7eb0c
 
 
 
 
 
 
08818fa
 
 
 
 
 
2a7eb0c
 
 
 
08818fa
 
 
 
 
 
2a7eb0c
 
 
 
08818fa
2a7eb0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/* ===== MusicMood — logika frontendu ===== */

const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const flash = document.getElementById("flash");
const cameraMsg = document.getElementById("cameraMsg");
const cameraMsgText = document.getElementById("cameraMsgText");
const enableCamBtn = document.getElementById("enableCamBtn");
const cameraSelect = document.getElementById("cameraSelect");
const scanBtn = document.getElementById("scanBtn");
const appPassInput = document.getElementById("appPass");
const statusEl = document.getElementById("status");
const resultsEl = document.getElementById("results");
const tvScreen = document.getElementById("tvScreen");
const tvStatic = document.getElementById("tvStatic");
const tvLed = document.getElementById("tvLed");

let currentStream = null;

const DNI = ["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"];

/* ---------- kamery ---------- */

async function startStream(deviceId) {
  if (currentStream) currentStream.getTracks().forEach((t) => t.stop());
  const constraints = {
    video: deviceId
      ? { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 960 } }
      : { width: { ideal: 1280 }, height: { ideal: 960 } },
    audio: false,
  };
  currentStream = await navigator.mediaDevices.getUserMedia(constraints);
  video.srcObject = currentStream;
  cameraMsg.classList.add("hidden");
  scanBtn.disabled = false;
}

async function listCameras() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const cams = devices.filter((d) => d.kind === "videoinput");
  cameraSelect.innerHTML = "";
  cams.forEach((cam, i) => {
    const opt = document.createElement("option");
    opt.value = cam.deviceId;
    opt.textContent = cam.label || `Kamera ${i + 1}`;
    cameraSelect.appendChild(opt);
  });
  return cams;
}

async function initCameras() {
  if (!navigator.mediaDevices?.getUserMedia) {
    cameraMsgText.textContent = "Twoja przeglądarka nie obsługuje dostępu do kamery.";
    return;
  }
  try {
    // najpierw poproś o dostęp — bez tego enumerateDevices nie zwraca nazw kamer
    await startStream(null);
    const cams = await listCameras();
    if (cams.length === 0) {
      cameraMsgText.textContent = "Nie wykryto żadnej kamery w systemie.";
      enableCamBtn.hidden = true;
      cameraMsg.classList.remove("hidden");
      return;
    }
    // zaznacz w select aktywną kamerę
    const activeId = currentStream.getVideoTracks()[0].getSettings().deviceId;
    if (activeId) cameraSelect.value = activeId;
  } catch (err) {
    // część przeglądarek blokuje kamerę bez gestu użytkownika —
    // pokaż przycisk, którego kliknięcie ponawia próbę
    cameraMsgText.textContent =
      `Kamera nie wystartowała automatycznie (${err.name}${err.message ? ": " + err.message : ""}). ` +
      "Kliknij poniżej i zezwól na dostęp.";
    enableCamBtn.hidden = false;
    cameraMsg.classList.remove("hidden");
  }
}

enableCamBtn.addEventListener("click", () => {
  cameraMsgText.textContent = "Uruchamianie kamery…";
  enableCamBtn.hidden = true;
  initCameras();
});

cameraSelect.addEventListener("change", async () => {
  try {
    await startStream(cameraSelect.value);
  } catch {
    cameraMsgText.textContent = "Nie udało się uruchomić wybranej kamery.";
    cameraMsg.classList.remove("hidden");
  }
});

/* ---------- geolokalizacja ---------- */

function getPosition() {
  return new Promise((resolve) => {
    if (!navigator.geolocation) return resolve(null);
    navigator.geolocation.getCurrentPosition(
      (pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
      () => resolve(null),
      { timeout: 6000, maximumAge: 600000 }
    );
  });
}

/* ---------- zdjęcie ---------- */

function captureFrame() {
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  canvas.getContext("2d").drawImage(video, 0, 0);
  // dataURL -> czysty base64
  return canvas.toDataURL("image/jpeg", 0.85).split(",")[1];
}

function doFlash() {
  flash.classList.add("on");
  setTimeout(() => flash.classList.remove("on"), 60);
}

/* ---------- retro logi na ekranie TV ---------- */

const tvLog = document.getElementById("tvLog");

const IDLE_LOGS = [
  "MOODTRON BIOS v0.62 ........ OK",
  "strojenie anteny do fal nastroju…",
  "odkurzanie lamp kineskopu…",
  "nasłuch eteru: cisza, tylko szum kosmiczny",
  "kalibracja gałki nastroju… ćwierć obrotu w prawo",
  "czekam na sygnał… może herbatka?",
  "liczę owce w paśmie UHF… doliczyłem do 42",
  "prasowanie pasków testowych…",
  "ziewam w częstotliwości 50 Hz…",
  "sprawdzam, czy ktoś patrzy… chyba tak",
  "smarowanie kondensatorów olejem lnianym…",
  "trening kineskopu: wdech, wydech…",
  "polerowanie elektronów na zapas…",
  "nuda… nawet szum ma dziś wolne",
  "wciąż gotowy. wciśnij SCAN, nie wstydź się",
];

const SCAN_LOGS = [
  "przechwytywanie fotonów twarzy…",
  "wysyłanie pikseli do mózgu elektronowego…",
  "GPT marszczy brwi nad Twoimi brwiami…",
  "mierzenie kącików ust suwmiarką…",
  "konsultacja z działem emocji…",
  "sprawdzanie ciśnienia (atmosferycznego, nie Twojego)…",
  "pytam chmury, jaka pogoda… dosłownie",
  "przeszukiwanie szafy z winylami…",
  "DJ MoodTron przegląda skrzynki z płytami…",
  "strzepywanie kurzu z igły gramofonu…",
  "negocjacje z YouTube w sprawie sygnału…",
  "dostrajanie tempa do Twojego pulsu…",
  "liczenie BPM na liczydle…",
  "przesłuchanie sekcji rytmicznej w toku…",
  "kalibrowanie nastroju do dnia tygodnia…",
  "muzy zwołane na naradę…",
];

let logTimer = null;
let logPool = [];
let logIdx = 0;

function logLine() {
  const t = new Date().toLocaleTimeString("pl-PL");
  const line = document.createElement("div");
  line.textContent = `[${t}] ${logPool[logIdx % logPool.length]}`;
  logIdx++;
  tvLog.appendChild(line);
  while (tvLog.children.length > 9) tvLog.removeChild(tvLog.firstChild);
}

function startLogs(mode) {
  clearInterval(logTimer);
  logPool = [...(mode === "scan" ? SCAN_LOGS : IDLE_LOGS)]
    .sort(() => Math.random() - 0.5);
  logIdx = 0;
  logLine();
  logTimer = setInterval(logLine, mode === "scan" ? 1100 : 2600);
}

function stopLogs() {
  clearInterval(logTimer);
  logTimer = null;
}

/* ---------- TV (YouTube IFrame API) ---------- */

let ytPlayer = null;
let ytReady = false;
let videoQueue = [];
let queueIndex = 0;

window.onYouTubeIframeAPIReady = () => { ytReady = true; };

function playNextInQueue() {
  queueIndex++;
  if (queueIndex < videoQueue.length) {
    setStatus(`Film niedostępny — próbuję następny wynik (${queueIndex + 1}/${videoQueue.length})…`, "busy");
    ytPlayer.loadVideoById(videoQueue[queueIndex]);
  } else {
    setStatus("Żaden ze znalezionych filmów nie jest dostępny do osadzenia. Kliknij SCAN ponownie.", "error");
  }
}

function playOnTV(videoIds) {
  videoQueue = videoIds;
  queueIndex = 0;
  stopLogs();
  tvStatic.classList.add("off");
  tvLed.classList.add("on");

  if (ytPlayer) {
    ytPlayer.loadVideoById(videoIds[0]);
    return;
  }
  if (!ytReady || !window.YT) {
    setStatus("Player YouTube jeszcze się ładuje — spróbuj za chwilę.", "error");
    return;
  }
  ytPlayer = new YT.Player("ytplayer", {
    width: "100%",
    height: "100%",
    videoId: videoIds[0],
    playerVars: { autoplay: 1, rel: 0 },
    events: {
      onReady: (e) => e.target.playVideo(),
      onError: playNextInQueue,
    },
  });
}

/* ---------- wyniki ---------- */

function setText(id, value) {
  document.getElementById(id).textContent = value ?? "—";
}

function showResults(data) {
  const o = data.osoba;
  setText("rPlec", o?.plec);
  setText("rWiek", o?.wiek);
  setText("rNastroj", o?.nastroj);
  setText("rEmocje", o?.emocje?.join(", "));

  const k = data.kontekst || {};
  setText("rCzas", `${k.dzien_tygodnia}, ${k.czas_lokalny}`);
  const p = k.pogoda;
  if (p && typeof p === "object") {
    const city = p.miejscowosc ? `${p.miejscowosc}: ` : "";
    setText("rPogoda", `${city}${p.opis}, ${p.temperatura_c}°C, wiatr ${p.wiatr_kmh} km/h`);
    setText("rCisnienie", p.cisnienie_hpa ? `${Math.round(p.cisnienie_hpa)} hPa` : "—");
  } else {
    setText("rPogoda", "brak danych");
    setText("rCisnienie", "—");
  }

  const r = data.rekomendacja || {};
  setText("rStyl", r.styl);
  setText("rTempo", r.tempo);
  setText("rRytmika", r.rytmika);
  setText("rCharakter", r.charakter);
  setText("rUzasadnienie", r.uzasadnienie);
  setText("rQuery", data.youtube_query);

  resultsEl.hidden = false;
}

function setStatus(text, cls = "") {
  statusEl.textContent = text;
  statusEl.className = "status " + cls;
}

/* ---------- SCAN ---------- */

scanBtn.addEventListener("click", async () => {
  const password = appPassInput.value.trim();
  if (!password) {
    setStatus("Wpisz najpierw hasło dostępu.", "error");
    appPassInput.focus();
    return;
  }
  if (!video.videoWidth) {
    setStatus("Kamera jeszcze się nie uruchomiła.", "error");
    return;
  }

  scanBtn.disabled = true;
  doFlash();
  const imageB64 = captureFrame();

  // TV wraca na "szum" z logami analizy; ewentualna muzyka stop
  if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo();
  tvLed.classList.remove("on");
  tvStatic.classList.remove("off");
  startLogs("scan");

  setStatus("Pobieranie lokalizacji i analiza zdjęcia (GPT-4o)…", "busy");
  const pos = await getPosition();

  const now = new Date();
  const payload = {
    password: password,
    image_b64: imageB64,
    local_time: now.toLocaleString("pl-PL", { dateStyle: "short", timeStyle: "short" }),
    day_of_week: DNI[now.getDay()],
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    lat: pos?.lat ?? null,
    lon: pos?.lon ?? null,
  };

  try {
    const resp = await fetch("/api/analyze", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    const data = await resp.json();
    if (!resp.ok) throw new Error(data.detail || "Błąd serwera");

    showResults(data);
    if (data.video_ids && data.video_ids.length > 0) {
      playOnTV(data.video_ids);
      setStatus("Gotowe! Utwór gra na MoodTronie 📺 (jeśli nie wystartował sam — kliknij ▶ na ekranie TV).");
    } else {
      setStatus("Analiza gotowa, ale nie udało się znaleźć utworu na YouTube — spróbuj ponownie.", "error");
      startLogs("idle");
    }
  } catch (err) {
    setStatus("Błąd: " + err.message, "error");
    startLogs("idle");
  } finally {
    scanBtn.disabled = false;
  }
});

/* ---------- start ---------- */
initCameras();
startLogs("idle");
navigator.mediaDevices?.addEventListener?.("devicechange", listCameras);