Spaces:
Running
Running
import gradio as gr | |
# 1) Your Hugging Face Space name: | |
SPACE_USERNAME = "hyungonryu" | |
SPACE_NAME = "GoeStationaryVFI" | |
# 2) Base URL for raw MP4 files via "resolve/main": | |
BASE_URL = f"https://huggingface.co/spaces/{SPACE_USERNAME}/{SPACE_NAME}/resolve/main" | |
html_code = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Video Sync with Custom UI</title> | |
<style> | |
body {{ font-family: sans-serif; margin: 20px; }} | |
table {{ margin: 0 auto; }} | |
td {{ padding: 5px; vertical-align: middle; }} | |
.slider, .timeline-slider {{ }} | |
.video-container-wrapper {{ position: relative; display: inline-block; }} | |
.video-container {{ | |
position: relative; | |
width: 640px; | |
height: 480px; | |
background: #000; | |
overflow: hidden; | |
}} | |
.video {{ | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 640px; | |
height: 480px; | |
object-fit: cover; | |
}} | |
.wipe-line {{ | |
position: absolute; | |
top: 0; | |
width: 2px; | |
height: 100%; | |
background: blue; | |
pointer-events: none; | |
}} | |
</style> | |
</head> | |
<body> | |
<center> | |
<h1>Geostationary Satellite Time Interpolation</h1> | |
<h4>Hyun Gon Ryu | NVIDIA AI Technology Center Korea</h4> | |
</center> | |
<table> | |
<tr> | |
<td width="700"> | |
<font size="1"> | |
This demo video consists of images generated at one-minute intervals from the KMA GK2A satellite's full-disk images, | |
originally captured at ten-minute intervals, using the VFI technique. | |
By using the wipe scrollbar, you can compare the original video (delineated by a blue line) with the enhanced version. | |
You can adjust the zoom and playback speed, and when played at 0.3x speed, the differences between the two videos | |
become distinctly apparent. | |
</font> | |
</td> | |
</tr> | |
</table> | |
<!-- Video set selection --> | |
<table id="videoSetTable"> | |
<tr> | |
<td> | |
Select Video Set: | |
<select id="videoSetSelect"> | |
<option value="">No</option> | |
<option value="0">TongA Case | 20220116 | ir105</option> | |
<option value="1">SouthAsia | 20240820 | ir105</option> | |
<option value="2">EastAsia | 20240820 | ir105</option> | |
<option value="3">SouthWestAsia | 20240820 | ir105</option> | |
</select> | |
</td> | |
</tr> | |
</table> | |
<table id="controlTable"> | |
<!-- 1st Row: Play button, Speed, Screen Size --> | |
<tr> | |
<td> | |
<button id="playPauseBtn">PLAY</button> | |
</td> | |
<td> | |
Screen Size: | |
<select id="scaleSelect"> | |
<option value="0.5">0.5</option> | |
<option value="1" selected>1</option> | |
<option value="1.5">1.5</option> | |
<option value="2">2</option> | |
</select> | |
</td> | |
<td> | |
Speed: | |
<select id="speedSelect"> | |
<option value="0.1">0.1</option> | |
<option value="0.2">0.2</option> | |
<option value="0.3">0.3</option> | |
<option value="0.4">0.4</option> | |
<option value="0.5">0.5</option> | |
<option value="0.6">0.6</option> | |
<option value="0.7">0.7</option> | |
<option value="0.8">0.8</option> | |
<option value="0.9">0.9</option> | |
<option value="1" selected>1</option> | |
<option value="2">2</option> | |
<option value="4">4</option> | |
</select> | |
</td> | |
</tr> | |
<!-- 2nd Row: Wipe Slider --> | |
<tr> | |
<td colspan="3"> | |
<input type="range" id="wipeRange" class="slider" min="0" max="1" step="0.01" value="0.5"> | |
</td> | |
</tr> | |
<!-- 3rd Row: Timeline Slider --> | |
<tr> | |
<td colspan="3"> | |
<input type="range" id="timelineRange" class="timeline-slider" min="0" max="100" step="0.1" value="0"> | |
</td> | |
</tr> | |
<!-- 4th Row: Video Area --> | |
<tr> | |
<td colspan="3"> | |
<div class="video-container-wrapper"> | |
<div class="video-container" id="videoContainer"> | |
<!-- Default placeholders --> | |
<video id="video1" class="video" src="" preload="auto" muted></video> | |
<video id="video2" class="video" src="" preload="auto" muted></video> | |
<div id="wipeLine" class="wipe-line"></div> | |
</div> | |
</div> | |
</td> | |
</tr> | |
</table> | |
<script> | |
// Base width remains constant. | |
const baseWidth = 640; | |
// We'll store the base "resolve/main" URL from Python as a JS var: | |
const BASE_URL = "{BASE_URL}"; | |
// Predefined video sets. | |
const videoSets = [ | |
{{ label: "TongA Case", area: "TongaArea", date: "20220116", sensor: "ir105" }}, | |
{{ label: "SouthAsia", area: "SouthAsia", date: "20240820", sensor: "ir105" }}, | |
{{ label: "EastAsia", area: "EastAsia", date: "20240820", sensor: "ir105" }}, | |
{{ label: "SouthWestAsia", area: "WestAsia", date: "20240820", sensor: "ir105" }}, | |
]; | |
// Get DOM elements. | |
const video1 = document.getElementById("video1"); | |
const video2 = document.getElementById("video2"); | |
const playPauseBtn = document.getElementById("playPauseBtn"); | |
const wipeRange = document.getElementById("wipeRange"); | |
const timelineRange = document.getElementById("timelineRange"); | |
const scaleSelect = document.getElementById("scaleSelect"); | |
const speedSelect = document.getElementById("speedSelect"); | |
const videoContainer = document.getElementById("videoContainer"); | |
const wipeLine = document.getElementById("wipeLine"); | |
const controlTable = document.getElementById("controlTable"); | |
const videoSetSelect = document.getElementById("videoSetSelect"); | |
let isPlaying = false; | |
// Update playback speed. | |
function updateSpeed() {{ | |
const speedValue = parseFloat(speedSelect.value); | |
video1.playbackRate = speedValue; | |
video2.playbackRate = speedValue; | |
}} | |
// Update the wipe effect. | |
function updateWipe() {{ | |
const wipeValue = parseFloat(wipeRange.value); | |
const rightPercent = (1 - wipeValue) * 100; | |
video2.style.clipPath = `inset(0 ${{rightPercent}}% 0 0)`; | |
// Calculate position using the base width. | |
const lineX = baseWidth * wipeValue - (wipeLine.clientWidth / 2); | |
wipeLine.style.left = `${{lineX}}px`; | |
}} | |
// Update timeline slider based on video progress. | |
function updateTimeline() {{ | |
if (video1.duration) {{ | |
const percent = (video1.currentTime / video1.duration) * 100; | |
timelineRange.value = percent; | |
}} | |
}} | |
video1.addEventListener("timeupdate", updateTimeline); | |
// Toggle play/pause. | |
function togglePlayPause() {{ | |
if (!isPlaying) {{ | |
video1.play(); | |
video2.play(); | |
playPauseBtn.textContent = "Pause"; | |
isPlaying = true; | |
}} else {{ | |
video1.pause(); | |
video2.pause(); | |
playPauseBtn.textContent = "Play"; | |
isPlaying = false; | |
}} | |
}} | |
// Seek both videos. | |
function seekTimeline() {{ | |
if (video1.duration) {{ | |
const percent = timelineRange.value / 100; | |
const newTime = video1.duration * percent; | |
video1.currentTime = newTime; | |
video2.currentTime = newTime; | |
}} | |
}} | |
// Update scale and adjust related elements. | |
function updateScale() {{ | |
const scaleValue = parseFloat(scaleSelect.value); | |
videoContainer.style.transform = `scale(${{scaleValue}})`; | |
videoContainer.style.transformOrigin = "top left"; | |
// Update slider and table widths to the visually scaled size. | |
const effectiveWidth = baseWidth * scaleValue; | |
wipeRange.style.width = effectiveWidth + "px"; | |
timelineRange.style.width = effectiveWidth + "px"; | |
controlTable.style.width = effectiveWidth + "px"; | |
updateWipe(); | |
}} | |
// Update video sources based on selected video set. | |
function updateVideoSet() {{ | |
const selectedIndex = videoSetSelect.value; | |
if (selectedIndex === "") {{ | |
// "No" selected: clear sources or do nothing. | |
video1.removeAttribute("src"); | |
video2.removeAttribute("src"); | |
video1.load(); | |
video2.load(); | |
return; | |
}} | |
const videoSet = videoSets[parseInt(selectedIndex)]; | |
const area = videoSet.area; | |
const date = videoSet.date; | |
const sensor = videoSet.sensor; | |
// Use "resolve/main" URLs: | |
const originalFile = `${{BASE_URL}}/${{area}}/video_${{area}}_${{sensor}}um_${{date}}_10min_010fps.mp4`; | |
const enhancedFile = `${{BASE_URL}}/${{area}}/video_${{area}}_${{sensor}}um_${{date}}_01min_100fps.mp4`; | |
// Update video sources. | |
video1.setAttribute("src", originalFile); | |
video2.setAttribute("src", enhancedFile); | |
// Reload the videos. | |
video1.load(); | |
video2.load(); | |
}} | |
// Event listeners. | |
playPauseBtn.addEventListener("click", togglePlayPause); | |
wipeRange.addEventListener("input", updateWipe); | |
timelineRange.addEventListener("input", seekTimeline); | |
scaleSelect.addEventListener("change", updateScale); | |
speedSelect.addEventListener("change", updateSpeed); | |
videoSetSelect.addEventListener("change", updateVideoSet); | |
// Initial setup. | |
updateWipe(); | |
updateScale(); | |
updateSpeed(); | |
</script> | |
</body> | |
</html> | |
""" | |
# Build the Gradio app with just an HTML component. | |
def build_app(): | |
with gr.Blocks() as demo: | |
gr.HTML(html_code) | |
return demo | |
demo = build_app() | |
if __name__ == "__main__": | |
demo.launch() | |