|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Camera Interaction App</title> |
|
<style> |
|
body { |
|
font-family: sans-serif; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
gap: 20px; |
|
padding: 20px; |
|
background-color: #f0f0f0; |
|
} |
|
.controls, .io-areas { |
|
display: flex; |
|
gap: 10px; |
|
align-items: center; |
|
background-color: #fff; |
|
padding: 15px; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
} |
|
.io-areas { |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
textarea { |
|
width: 300px; |
|
height: 80px; |
|
padding: 8px; |
|
border: 1px solid #ccc; |
|
border-radius: 4px; |
|
font-size: 14px; |
|
} |
|
#videoFeed { |
|
width: 480px; |
|
height: 360px; |
|
border: 2px solid #333; |
|
background-color: #000; |
|
border-radius: 8px; |
|
} |
|
#startButton { |
|
padding: 10px 20px; |
|
font-size: 16px; |
|
cursor: pointer; |
|
border: none; |
|
border-radius: 4px; |
|
color: white; |
|
} |
|
#startButton.start { |
|
background-color: #28a745; |
|
} |
|
#startButton.stop { |
|
background-color: #dc3545; |
|
} |
|
label { |
|
font-weight: bold; |
|
} |
|
select { |
|
padding: 8px; |
|
border-radius: 4px; |
|
border: 1px solid #ccc; |
|
} |
|
.hidden { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<h1>Camera Interaction App</h1> |
|
|
|
<video id="videoFeed" autoplay playsinline></video> |
|
<canvas id="canvas" class="hidden"></canvas> |
|
|
|
<div class="io-areas"> |
|
<div> |
|
<label for="instructionText">Instruction:</label><br> |
|
<textarea id="instructionText" style="height: 2em; width: 40em" name="Instruction"></textarea> |
|
</div> |
|
<div> |
|
<label for="responseText">Response:</label><br> |
|
<textarea id="responseText" style="height: 2em; width: 40em" name="Response" readonly placeholder="Server response will appear here..."></textarea> |
|
</div> |
|
</div> |
|
|
|
<div class="controls"> |
|
<label for="intervalSelect">Interval between 2 requests:</label> |
|
<select id="intervalSelect" name="Interval between 2 requests"> |
|
<option value="0" selected>0ms</option> |
|
<option value="100">100ms</option> |
|
<option value="250">250ms</option> |
|
<option value="500">500ms</option> |
|
<option value="1000">1s</option> |
|
<option value="2000">2s</option> |
|
</select> |
|
<button id="startButton" class="start">Start</button> |
|
</div> |
|
|
|
<script type="module"> |
|
import { |
|
AutoProcessor, |
|
AutoModelForVision2Seq, |
|
RawImage |
|
} from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers/dist/transformers.min.js'; |
|
|
|
const video = document.getElementById('videoFeed'); |
|
const canvas = document.getElementById('canvas'); |
|
const instructionText = document.getElementById('instructionText'); |
|
const responseText = document.getElementById('responseText'); |
|
const intervalSelect = document.getElementById('intervalSelect'); |
|
const startButton = document.getElementById('startButton'); |
|
|
|
instructionText.value = "In one sentence, what do you see?"; |
|
|
|
let stream; |
|
let isProcessing = false; |
|
|
|
let processor, model; |
|
|
|
async function initModel() { |
|
const modelId = 'HuggingFaceTB/SmolVLM-Instruct'; |
|
processor = await AutoProcessor.from_pretrained(modelId); |
|
model = await AutoModelForVision2Seq.from_pretrained(modelId, { |
|
dtype: { |
|
embed_tokens: 'fp16', |
|
vision_encoder: 'q4', |
|
decoder_model_merged: 'q4' |
|
}, |
|
device: "webgpu", |
|
}); |
|
} |
|
|
|
async function initCamera() { |
|
try { |
|
stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); |
|
video.srcObject = stream; |
|
responseText.value = "Camera access granted. Ready to start."; |
|
} catch (err) { |
|
console.error("Error accessing camera:", err); |
|
responseText.value = `Error accessing camera: ${err.name} - ${err.message}. Please ensure permissions are granted and you are on HTTPS or localhost.`; |
|
alert(`Error accessing camera: ${err.name}. Make sure you've granted permission and are on HTTPS or localhost.`); |
|
} |
|
} |
|
|
|
function captureImage() { |
|
if (!stream || !video.videoWidth) { |
|
console.warn("Video stream not ready for capture."); |
|
return null; |
|
} |
|
canvas.width = video.videoWidth; |
|
canvas.height = video.videoHeight; |
|
const context = canvas.getContext('2d', { willReadFrequently: true }); |
|
context.drawImage(video, 0, 0, canvas.width, canvas.height); |
|
const frame = context.getImageData(0, 0, canvas.width, canvas.height); |
|
return new RawImage(frame.data, frame.width, frame.height, 4); |
|
} |
|
|
|
async function runLocalVisionInference(imgElement, instruction) { |
|
const messages = [{ |
|
role: 'user', |
|
content: [ |
|
{ type: 'image' }, |
|
{ type: 'text', text: instruction } |
|
] |
|
}]; |
|
const text = processor.apply_chat_template(messages, { add_generation_prompt: true }); |
|
const inputs = await processor(text, [imgElement], { do_image_splitting: false }); |
|
const generatedIds = await model.generate({ ...inputs, max_new_tokens: 100 }); |
|
const output = processor.batch_decode( |
|
generatedIds.slice(null, [inputs.input_ids.dims.at(-1), null]), |
|
{ skip_special_tokens: true } |
|
); |
|
return output[0].trim(); |
|
} |
|
|
|
async function sendData() { |
|
if (!isProcessing) return; |
|
const instruction = instructionText.value; |
|
const rawImg = captureImage(); |
|
if (!rawImg) { |
|
responseText.value = 'Capture failed'; |
|
return; |
|
} |
|
try { |
|
const reply = await runLocalVisionInference(rawImg, instruction); |
|
responseText.value = reply; |
|
} catch (e) { |
|
console.error(e); |
|
responseText.value = `Error: ${e.message}`; |
|
} |
|
} |
|
|
|
function sleep(ms) { |
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
} |
|
|
|
async function processingLoop() { |
|
const intervalMs = parseInt(intervalSelect.value, 10); |
|
while (isProcessing) { |
|
await sendData(); |
|
if (!isProcessing) break; |
|
await sleep(intervalMs); |
|
} |
|
} |
|
|
|
function handleStart() { |
|
if (!stream) { |
|
responseText.value = "Camera not available. Cannot start."; |
|
alert("Camera not available. Please grant permission first."); |
|
return; |
|
} |
|
isProcessing = true; |
|
startButton.textContent = "Stop"; |
|
startButton.classList.replace('start', 'stop'); |
|
|
|
instructionText.disabled = true; |
|
intervalSelect.disabled = true; |
|
|
|
responseText.value = "Processing started..."; |
|
|
|
processingLoop(); |
|
} |
|
|
|
function handleStop() { |
|
isProcessing = false; |
|
startButton.textContent = "Start"; |
|
startButton.classList.replace('stop', 'start'); |
|
|
|
instructionText.disabled = false; |
|
intervalSelect.disabled = false; |
|
if (responseText.value.startsWith("Processing started...")) { |
|
responseText.value = "Processing stopped."; |
|
} |
|
} |
|
|
|
startButton.addEventListener('click', () => { |
|
if (isProcessing) { |
|
handleStop(); |
|
} else { |
|
handleStart(); |
|
} |
|
}); |
|
|
|
window.addEventListener('DOMContentLoaded', async () => { |
|
await initModel(); |
|
await initCamera(); |
|
}); |
|
|
|
window.addEventListener('beforeunload', () => { |
|
if (stream) { |
|
stream.getTracks().forEach(track => track.stop()); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|