[Vibe Coding] 网红圣诞树改造音乐播放器
[Vibe Coding] 网红圣诞树改造音乐播放器
[Vibe Coding] 网红圣诞树改造音乐播放器 [Vibe Coding] 网红圣诞树改造音乐播放器 Modified December 15, 2025 👨💻 作者:吵爷 把之前群里的手势圣诞树项目改了下,做成了一个本地音乐播放器,适合玩Suno的朋友拿来批量听小样,且不用手动切到页面里去来回切歌或者鼠标去一个个点自己喜欢的Demo,方便后续批量筛选整理。为了方便做成了单文件脚本,不用麻烦去Git。 初始页面(首次加载需要自动下载MediaPipe的依赖文件) 左边的按钮找到小样存放的文件夹批量上传 功能 上传完成后有几个互动手势: ☝一根手指(食指):切下一首歌 ✌两根手指(食指+中指):回到上一首歌 👍大拇指:点赞 (标记好的Demo) 这个场景不需要来回切Suno的页面,可以同时去忙其他事情,开个Web和摄像头就可以了。然后批量听了以后,如果想把所有点赞的小样收集起来 识别到第一次点赞后,左边会跳出来一个Rename Script按钮(因为网页安全限制,不允许自动给文件改名),下载下来放到歌单的上级文件夹,点击下载的bat文件,就可以批量给点赞过的小样重命名了,方便后续筛选。 完整代码(单文件HTML) Code block HTML document.body.removeChild(a); URL.revokeObjectURL(url); }; // MediaPipe async function setupMediaPipe() { try { const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks vision@0.10.8/wasm"); gestureRecognizer = await GestureRecognizer.createFromOptions(vision, { baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe models/gesture recognizer/gesture recognizer/float16/1/gesture recognizer.task", delegate: "GPU" }, runningMode: "VIDEO", numHands: 1 }); const video = document.getElementById('webcam'); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); video.srcObject = stream; video.addEventListener('loadeddata', () = { webcamRunning = true; video.width = video.videoWidth; video.height = video.videoHeight; document.getElementById('loading msg').textContent = "READY!"; document.getElementById('webcam status').textContent = "Tracking Active"; // FIX: Start loop ONLY when data is loaded // Use setTimeout to allow background execution setTimeout(predictWebcam, 100); setTimeout(() = { const l = document.getElementById('loader'); l.style.opacity = '0'; setTimeout(() = l.style.display = 'none', 1000); }, 500); }); video.play(); } catch(err) { console.warn("Gesture init failed:", err); const l = document.getElementById('loader'); document.getElementById('loading msg').textContent = "Mouse Only Mode"; setTimeout(() = { l.style.opacity = '0'; setTimeout(() = l.style.display = 'none', 1000); }, 1500); } } function predictWebcam() { if (!webcamRunning || !gestureRecognizer) { setTimeout(predictWebcam, 100); return; } const video = document.getElementById('webcam'); const nowInMs = Date.now(); if (video.currentTime !== lastVideoTime) { lastVideoTime = video.currentTime; try { const results = gestureRecognizer.recognizeForVideo(video, nowInMs); const container = document.getElementById('webcam container'); if (results.gestures.length 0) { container.classList.add('active'); const gesture = results.gestures[0][0].categoryName; const score = results.gestures[0][0].score; if (score 0.5 && (nowInMs lastGestureTime GESTURE COOLDOWN)) { console.log("Gesture:", gesture); lastGestureTime = nowInMs; if (gesture === "Pointing Up") nextSong(); else if (gesture === "Victory") prevSong(); else if (gesture === "Thumb Up") likeSong(); } } else { container.classList.remove('active'); } } catch(e) { console.warn("Recognition error:", e); } } // Re schedule with setTimeout to keep running in background setTimeout(predictWebcam, 100); // 10 FPS is enough } // Animation Loop const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const time = clock.getElapsedTime(); if (treeMaterial) { treeMaterial.uniforms.uTime.value = time; } controls.update(); const intensity = (Math.sin(time (Math.PI / 15)) + 1) / 2; const targetActive = 200 + intensity (CONFIG.snowMax 200); activeSnowCount += (targetActive activeSnowCount) 0.05; if(snow) { const positions = snow.geometry.attributes.position.array; const velocities = snow.userData.velocities; for (let i = 0; i < CONFIG.snowMax; i++) { positions[i 3 + 1] = velocities[i]; if (positions[i 3 + 1] < 25) { if (i < activeSnowCount) { positions[i 3 + 1] = 25; positions[i 3] = (Math.random() 0.5) 60; positions[i 3 + 2] = (Math.random() 0.5) 60; } else { positions[i 3 + 1] = 100; } } } snow.geometry.attributes.position.needsUpdate = true; } if (starMesh) { starMesh.rotation.y = time 0.5; starMesh.rotation.z = Math.sin(time 0.5) 0.1; const glow = starMesh.children[0]; glow.scale.set(12 + Math.sin(time 3) 2, 12 + Math.sin(time 3) 2, 1); } tree.rotation.y = time 0.05; updateAlbumArt(time); renderer.render(scene, camera); } window.addEventListener('resize', () = { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); setTimeout(() = { const l = document.getElementById('loader'); if (l && getComputedStyle(l).display !== 'none') { console.warn("Loader timeout"); document.getElementById('loading msg').textContent = "Starting anyway..."; setTimeout(() = { l.style.display = 'none'; }, 1000); } }, 8000); setupMediaPipe(); animate(); </script </body </html 👨💻 作者:吵爷 把之前群里的手势圣诞树项目改了下,做成了一个本地音乐播放器,适合玩Suno的朋友拿来批量听小样,且不用手动切到页面里去来回切歌或者鼠标去一个个点自己喜欢的Demo,方便后续批量筛选整理。为了方便做成了单文件脚本,不用麻烦去Git。 作者:吵爷 把之前群里的手势圣诞树项目改了下,做成了一个本地音乐播放器,适合玩Suno的朋友拿来批量听小样,且不用手动切到页面里去来回切歌或者鼠标去一个个点自己喜欢的Demo,方便后续批量筛选整理。为了方便做成了单文件脚本,不用麻烦去Git。 初始页面(首次加载需要自动下载MediaPipe的依赖文件) 左边的按钮找到小样存放的文件夹批量上传 功能 上传完成后有几个互动手势: ☝一根手指(食指):切下一首歌 ✌两根手指(食指+中指):回到上一首歌 👍大拇指:点赞 (标记好的Demo) 这个场景不需要来回切Suno的页面,可以同时去忙其他事情,开个Web和摄像头就可以了。然后批量听了以后,如果想把所有点赞的小样收集起来 识别到第一次点赞后,左边会跳出来一个Rename Script按钮(因为网页安全限制,不允许自动给文件改名),下载下来放到歌单的上级文件夹,点击下载的bat文件,就可以批量给点赞过的小样重命名了,方便后续筛选。 完整代码(单文件HTML)