| |
|
|
| 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"]; |
|
|
| |
|
|
| 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 { |
| |
| 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; |
| } |
| |
| const activeId = currentStream.getVideoTracks()[0].getSettings().deviceId; |
| if (activeId) cameraSelect.value = activeId; |
| } catch (err) { |
| |
| |
| 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"); |
| } |
| }); |
|
|
| |
|
|
| 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 } |
| ); |
| }); |
| } |
|
|
| |
|
|
| function captureFrame() { |
| canvas.width = video.videoWidth; |
| canvas.height = video.videoHeight; |
| canvas.getContext("2d").drawImage(video, 0, 0); |
| |
| return canvas.toDataURL("image/jpeg", 0.85).split(",")[1]; |
| } |
|
|
| function doFlash() { |
| flash.classList.add("on"); |
| setTimeout(() => flash.classList.remove("on"), 60); |
| } |
|
|
| |
|
|
| 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; |
| } |
|
|
| |
|
|
| 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, |
| }, |
| }); |
| } |
|
|
| |
|
|
| 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; |
| } |
|
|
| |
|
|
| 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(); |
|
|
| |
| 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; |
| } |
| }); |
|
|
| |
| initCameras(); |
| startLogs("idle"); |
| navigator.mediaDevices?.addEventListener?.("devicechange", listCameras); |
|
|