Created
May 15, 2025 01:29
-
-
Save masterkain/641e43c623e5e30081733a5fb56a563b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Enhanced Video File Interaction App</title> | |
<style> | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 20px; | |
padding: 20px; | |
background-color: #f4f7f9; /* Softer background */ | |
color: #333; | |
line-height: 1.6; | |
} | |
.app-container { | |
width: 100%; | |
max-width: 900px; /* Max width for the main content */ | |
display: flex; | |
flex-direction: column; | |
gap: 25px; | |
} | |
header { | |
width: 100%; | |
text-align: center; | |
margin-bottom: 10px; | |
} | |
h1 { | |
color: #2c3e50; /* Darker, more professional blue/grey */ | |
font-weight: 600; | |
font-size: 2.2em; | |
} | |
h2 { | |
color: #34495e; /* Slightly lighter than h1 */ | |
border-bottom: 2px solid #e0e0e0; | |
padding-bottom: 10px; | |
margin-top: 0; | |
margin-bottom: 20px; | |
font-size: 1.6em; | |
} | |
.card { | |
background-color: #fff; | |
padding: 25px; /* Increased padding */ | |
border-radius: 12px; /* More rounded */ | |
box-shadow: 0 5px 15px rgba(0,0,0,0.08); /* Softer, more diffused shadow */ | |
} | |
.video-setup video { | |
width: 100%; /* Make video responsive within its container */ | |
max-width: 700px; /* Max width for the video itself */ | |
height: auto; | |
border: 1px solid #ddd; | |
background-color: #000; | |
border-radius: 8px; | |
display: block; /* For centering if a max-width is set */ | |
margin: 0 auto 20px auto; /* Center video if it's smaller than container */ | |
} | |
.file-input-area { | |
display: flex; | |
flex-direction: column; /* Stack label and input/filename */ | |
gap: 10px; | |
align-items: flex-start; /* Align items to the start */ | |
margin-bottom: 20px; | |
} | |
.file-input-label { | |
background-color: #3498db; /* A pleasant blue */ | |
color: white; | |
padding: 10px 18px; | |
border-radius: 6px; | |
cursor: pointer; | |
display: inline-block; | |
font-size: 0.95em; | |
transition: background-color 0.2s ease; | |
} | |
.file-input-label:hover { | |
background-color: #2980b9; | |
} | |
#fileName { | |
margin-top: 5px; /* Space between button and filename */ | |
font-style: italic; | |
color: #555; | |
font-size: 0.9em; | |
} | |
.io-areas { | |
display: flex; | |
flex-direction: column; | |
gap: 20px; /* Increased gap for clarity */ | |
} | |
.io-areas div { | |
display: flex; | |
flex-direction: column; /* Stack label above input */ | |
} | |
textarea, input[type="text"], select { | |
width: 100%; | |
padding: 12px; /* More padding */ | |
border: 1px solid #dde1e4; | |
border-radius: 6px; | |
font-size: 0.95em; | |
box-sizing: border-box; | |
margin-top: 5px; /* Space between label and input */ | |
background-color: #fdfdfd; | |
} | |
textarea:focus, input[type="text"]:focus, select:focus { | |
border-color: #3498db; /* Accent color */ | |
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15); | |
outline: none; | |
} | |
label { | |
font-weight: 600; | |
color: #4a4a4a; /* Dark grey for labels */ | |
font-size: 1em; | |
} | |
.controls { | |
display: flex; | |
flex-wrap: wrap; /* Allow wrapping on smaller screens */ | |
gap: 20px; | |
align-items: flex-end; /* Align items to the bottom if they have different heights */ | |
margin-top: 25px; /* Space above the controls */ | |
} | |
.controls > div { /* Target the div wrapping label and select */ | |
flex-grow: 1; /* Allow interval select to take space */ | |
} | |
#startButton { | |
padding: 12px 25px; | |
font-size: 1.05em; | |
font-weight: bold; | |
cursor: pointer; | |
border: none; | |
border-radius: 6px; | |
color: white; | |
transition: background-color 0.2s ease-in-out, transform 0.1s ease; | |
min-width: 180px; /* Ensure button has a decent width */ | |
} | |
#startButton.start { background-color: #2ecc71; } /* Fresher green */ | |
#startButton.start:hover { background-color: #27ae60; } | |
#startButton.stop { background-color: #e74c3c; } /* Fresher red */ | |
#startButton.stop:hover { background-color: #c0392b; } | |
#startButton:disabled { | |
background-color: #bdc3c7; /* Neutral grey for disabled */ | |
cursor: not-allowed; | |
opacity: 0.8; | |
} | |
#startButton:not(:disabled):active { | |
transform: translateY(1px); | |
} | |
#responseText { | |
min-height: 3em; /* Ensure it's tall enough for a few lines */ | |
background-color: #f9f9f9; /* Slightly different background for readonly */ | |
} | |
.hidden { | |
display: none; | |
} | |
/* Responsive adjustments */ | |
@media (max-width: 768px) { | |
.app-container { | |
gap: 20px; | |
} | |
.card { | |
padding: 20px; | |
} | |
h1 { | |
font-size: 1.8em; | |
} | |
h2 { | |
font-size: 1.4em; | |
} | |
.controls { | |
flex-direction: column; | |
align-items: stretch; /* Make controls stack vertically */ | |
} | |
#startButton { | |
width: 100%; /* Full width button on small screens */ | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>Video Analysis Dashboard</h1> | |
</header> | |
<main class="app-container"> | |
<section class="card video-setup"> | |
<h2>Video Source & Player</h2> | |
<div class="file-input-area"> | |
<label for="videoFile" class="file-input-label">Choose Video File</label> | |
<input type="file" id="videoFile" accept="video/*" style="display: none;"> | |
<span id="fileName">No file chosen</span> | |
</div> | |
<video id="videoFeed" controls playsinline></video> | |
<canvas id="canvas" class="hidden"></canvas> | |
</section> | |
<section class="card analysis-config"> | |
<h2>Analysis Configuration</h2> | |
<div class="io-areas"> | |
<div> | |
<label for="baseURL">Base API Endpoint:</label> | |
<input type="text" id="baseURL" name="BaseAPI" value="http://localhost:8080"> | |
</div> | |
<div> | |
<label for="instructionText">Instruction for AI:</label> | |
<textarea id="instructionText" style="min-height: 3em;" name="Instruction"></textarea> | |
</div> | |
<div> | |
<label for="responseText">Live Response:</label> | |
<textarea id="responseText" name="Response" readonly placeholder="Server response will appear here..."></textarea> | |
</div> | |
</div> | |
<div class="controls"> | |
<div> | |
<label for="intervalSelect">Processing Interval:</label> | |
<select id="intervalSelect" name="Interval between 2 requests"> | |
<option value="100">100ms</option> | |
<option value="250">250ms</option> | |
<option value="500" selected>500ms</option> | |
<option value="1000">1s</option> | |
<option value="2000">2s</option> | |
<option value="5000">5s</option> | |
</select> | |
</div> | |
<button id="startButton" class="start" disabled>Start Processing</button> | |
</div> | |
</section> | |
</main> | |
<script> | |
const video = document.getElementById('videoFeed'); | |
const canvas = document.getElementById('canvas'); | |
const videoFile = document.getElementById('videoFile'); | |
const fileNameDisplay = document.getElementById('fileName'); // For custom file input | |
const baseURLInput = document.getElementById('baseURL'); | |
const instructionText = document.getElementById('instructionText'); | |
const responseText = document.getElementById('responseText'); | |
const intervalSelect = document.getElementById('intervalSelect'); | |
const startButton = document.getElementById('startButton'); | |
instructionText.value = "What do you see in this frame?"; // default instruction | |
let intervalId; | |
let isProcessing = false; | |
let currentObjectURL = null; | |
videoFile.addEventListener('change', function(event) { | |
if (isProcessing) { | |
handleStop(); | |
} | |
if (currentObjectURL) { | |
URL.revokeObjectURL(currentObjectURL); | |
} | |
const file = event.target.files[0]; | |
if (file) { | |
fileNameDisplay.textContent = file.name; // Update displayed filename | |
currentObjectURL = URL.createObjectURL(file); | |
video.src = currentObjectURL; | |
video.load(); | |
responseText.value = "Video loaded. Ready to start processing."; | |
startButton.disabled = true; // Keep disabled until 'loadeddata' | |
} else { | |
fileNameDisplay.textContent = "No file chosen"; | |
responseText.value = "No video file selected."; | |
startButton.disabled = true; | |
video.src = ""; | |
} | |
}); | |
async function sendChatCompletionRequest(instruction, imageBase64URL) { | |
const apiURL = `${baseURLInput.value}/v1/chat/completions`; | |
try { | |
const response = await fetch(apiURL, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
max_tokens: 150, // Slightly increased for potentially more descriptive answers | |
messages: [ | |
{ role: 'user', content: [ | |
{ type: 'text', text: instruction }, | |
{ type: 'image_url', image_url: { | |
url: imageBase64URL, | |
} } | |
] }, | |
] | |
}) | |
}); | |
if (!response.ok) { | |
const errorData = await response.text(); | |
console.error(`Server error: ${response.status} - ${errorData} at ${apiURL}`); | |
return `Server error: ${response.status} - ${errorData.substring(0,100)}...`; // Show snippet of error | |
} | |
const data = await response.json(); | |
if (data.choices && data.choices.length > 0 && data.choices[0].message) { | |
return data.choices[0].message.content; | |
} else { | |
console.error("Unexpected response structure:", data); | |
return "Received an unexpected response structure from the server."; | |
} | |
} catch (error) { | |
console.error(`Network error: ${error.message} while trying to reach ${apiURL}`); | |
return `Network error: ${error.message}`; | |
} | |
} | |
function captureImage() { | |
if (!video.videoWidth || video.ended) { | |
if (video.ended && isProcessing) { | |
responseText.value = "Video has ended. Stopping processing."; | |
handleStop(); | |
} | |
return null; | |
} | |
// Only capture if video is playing or explicitly paused but processing is active | |
if (video.paused && !isProcessing && video.currentTime === 0) { | |
// Don't capture if paused at the very beginning before start is pressed | |
// This condition might need tweaking based on desired behavior for paused videos | |
// return null; | |
} | |
canvas.width = video.videoWidth; | |
canvas.height = video.videoHeight; | |
const context = canvas.getContext('2d'); | |
context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
return canvas.toDataURL('image/jpeg', 0.75); // Adjusted quality slightly | |
} | |
async function sendData() { | |
if (!isProcessing) return; | |
if (video.ended) { | |
console.log("Video ended, stopping data send."); | |
handleStop(); | |
responseText.value = "Video finished. Processing automatically stopped."; | |
return; | |
} | |
const instruction = instructionText.value; | |
const imageBase64URL = captureImage(); | |
if (!imageBase64URL) { | |
if (!video.ended && !video.paused) { // Only show if not ended and not just paused | |
responseText.value = "Failed to capture image. Video might not be playing or fully loaded."; | |
} else if (video.paused && isProcessing){ | |
// If deliberately processing while paused, we might still want to send. | |
// For now, let it proceed if image capture was successful. | |
} | |
return; | |
} | |
try { | |
const response = await sendChatCompletionRequest(instruction, imageBase64URL); | |
responseText.value = response; | |
} catch (error) { | |
console.error('Error sending data:', error); | |
responseText.value = `Error: ${error.message}`; | |
} | |
} | |
function handleStart() { | |
if (!video.src || video.readyState < video.HAVE_FUTURE_DATA) { // HAVE_FUTURE_DATA (3) or HAVE_ENOUGH_DATA (4) | |
responseText.value = "Video not loaded or not ready. Cannot start."; | |
alert("Please select a video file and wait for it to load fully."); | |
return; | |
} | |
isProcessing = true; | |
startButton.textContent = "Stop Processing"; | |
startButton.classList.remove('start'); | |
startButton.classList.add('stop'); | |
instructionText.disabled = true; | |
intervalSelect.disabled = true; | |
videoFile.disabled = true; | |
baseURLInput.disabled = true; | |
document.querySelector('.file-input-label').classList.add('disabled-label'); | |
if (video.paused) { | |
video.play().catch(e => console.warn("Autoplay on start failed:", e.message)); | |
} | |
responseText.value = "Processing started..."; | |
const intervalMs = parseInt(intervalSelect.value, 10); | |
// Initial immediate call if video is ready | |
if (!video.paused || video.currentTime > 0) { | |
sendData(); // Send first frame immediately | |
} | |
intervalId = setInterval(sendData, intervalMs); | |
} | |
function handleStop() { | |
isProcessing = false; | |
if (intervalId) { | |
clearInterval(intervalId); | |
intervalId = null; | |
} | |
startButton.textContent = "Start Processing"; | |
startButton.classList.remove('stop'); | |
startButton.classList.add('start'); | |
if (video.src && video.readyState >= video.HAVE_METADATA) { | |
startButton.disabled = false; | |
} else { | |
startButton.disabled = true; | |
} | |
instructionText.disabled = false; | |
intervalSelect.disabled = false; | |
videoFile.disabled = false; | |
baseURLInput.disabled = false; | |
const fileLabel = document.querySelector('.file-input-label'); | |
if (fileLabel) fileLabel.classList.remove('disabled-label'); | |
if (responseText.value.startsWith("Processing started...") || responseText.value.startsWith("Sending frame...")) { | |
if (video.ended) { | |
responseText.value = "Video finished. Processing stopped."; | |
} else { | |
responseText.value = "Processing stopped by user."; | |
} | |
} | |
} | |
startButton.addEventListener('click', () => { | |
if (isProcessing) { | |
handleStop(); | |
} else { | |
handleStart(); | |
} | |
}); | |
video.addEventListener('ended', () => { | |
// responseText.value = "Video finished."; // This message is now handled in sendData or handleStop | |
if (isProcessing) { | |
// handleStop will be called by sendData, or if it wasn't, call it here. | |
// This ensures UI is reset correctly if sendData's interval already cleared. | |
if (isProcessing) { // check again as sendData might have set it to false | |
responseText.value = "Video finished. Processing automatically stopped."; | |
handleStop(); | |
} | |
} else { | |
responseText.value = "Video finished."; | |
// If a file is loaded, the start button should be enabled to re-process | |
if (videoFile.files.length > 0 && video.readyState >= video.HAVE_METADATA) { | |
startButton.disabled = false; | |
} | |
} | |
}); | |
video.addEventListener('error', (e) => { | |
console.error("Video Error:", e); | |
let errorMsg = 'Unknown video error.'; | |
if (e.target.error) { | |
switch (e.target.error.code) { | |
case e.target.error.MEDIA_ERR_ABORTED: | |
errorMsg = 'Video playback aborted.'; | |
break; | |
case e.target.error.MEDIA_ERR_NETWORK: | |
errorMsg = 'A network error caused video download to fail.'; | |
break; | |
case e.target.error.MEDIA_ERR_DECODE: | |
errorMsg = 'Video playback aborted due to a decoding error.'; | |
break; | |
case e.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED: | |
errorMsg = 'The video format is not supported.'; | |
break; | |
default: | |
errorMsg = 'An unknown error occurred with the video.'; | |
} | |
} | |
responseText.value = `Video error: ${errorMsg} Try a different video file.`; | |
fileNameDisplay.textContent = "Error with video file."; | |
startButton.disabled = true; | |
if (isProcessing) { | |
handleStop(); | |
} | |
}); | |
video.addEventListener('loadeddata', () => { | |
// console.log("Video data loaded, readyState:", video.readyState); | |
if (!isProcessing && videoFile.files.length > 0) { | |
startButton.disabled = false; | |
responseText.value = "Video ready. Click 'Start Processing'."; | |
} | |
}); | |
video.addEventListener('canplay', () => { | |
// console.log("Video can play, readyState:", video.readyState); | |
if (!isProcessing && videoFile.files.length > 0) { | |
startButton.disabled = false; | |
if (!responseText.value.includes("Video loaded") && !responseText.value.includes("Video ready")) { | |
responseText.value = "Video can play. Click 'Start Processing'."; | |
} | |
} | |
}); | |
window.addEventListener('beforeunload', () => { | |
if (currentObjectURL) { | |
URL.revokeObjectURL(currentObjectURL); | |
} | |
if (intervalId) { | |
clearInterval(intervalId); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment