Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save masterkain/641e43c623e5e30081733a5fb56a563b to your computer and use it in GitHub Desktop.

Select an option

Save masterkain/641e43c623e5e30081733a5fb56a563b to your computer and use it in GitHub Desktop.

Revisions

  1. masterkain created this gist May 15, 2025.
    533 changes: 533 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,533 @@
    <!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>