percent based resolution
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m9s

This commit is contained in:
2026-02-09 18:33:21 -05:00
parent cbd5de8cb3
commit e8eb1fe472
2 changed files with 116 additions and 39 deletions

13
app.py
View File

@@ -124,13 +124,12 @@ def process_video():
if width and height: if width and height:
filters.append(f"crop={width}:{height}:{x}:{y}") filters.append(f"crop={width}:{height}:{x}:{y}")
# Add scale filter if resolution specified # Add scale filter if scale percentage specified (not 100%)
resolution = data.get('resolution') scale_percentage = data.get('scale_percentage', 100)
if resolution: if scale_percentage != 100:
width = resolution.get('width') # Scale based on percentage - ffmpeg will apply this after crop
height = resolution.get('height') scale_filter = f"scale=iw*{scale_percentage/100}:ih*{scale_percentage/100}"
if width and height: filters.append(scale_filter)
filters.append(f"scale={width}:{height}")
# Apply filters if any # Apply filters if any
if filters: if filters:

View File

@@ -184,6 +184,63 @@
font-weight: 600; font-weight: 600;
} }
.scale-slider-container {
margin: 15px 0;
}
.scale-slider {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e9ecef;
outline: none;
-webkit-appearance: none;
}
.scale-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.scale-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.scale-display {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.scale-value {
font-size: 1.2em;
font-weight: 600;
color: #667eea;
}
.resolution-preview {
background: white;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9em;
color: #495057;
}
.timeline-container { .timeline-container {
margin: 15px 0; margin: 15px 0;
padding: 20px; padding: 20px;
@@ -513,24 +570,23 @@
</div> </div>
</div> </div>
<!-- Resolution Controls --> <!-- Scale Controls -->
<div class="form-group"> <div class="form-group">
<label>📐 Output Resolution</label> <label>📐 Output Scale</label>
<div class="preset-resolutions"> <div class="scale-slider-container">
<button class="preset-btn" data-width="3840" data-height="2160">4K (3840x2160)</button> <div class="scale-display">
<button class="preset-btn" data-width="1920" data-height="1080">1080p (1920x1080)</button> <span>Scale: <span class="scale-value" id="scale-value-display">100%</span></span>
<button class="preset-btn" data-width="1280" data-height="720">720p (1280x720)</button> <span id="resolution-info" style="color: #6c757d;">Output: <strong id="output-resolution">--</strong></span>
<button class="preset-btn" data-width="854" data-height="480">480p (854x480)</button>
<button class="preset-btn active" data-width="0" data-height="0">Original</button>
</div>
<div class="form-row">
<div>
<label style="font-weight: normal;">Width</label>
<input type="number" id="width" placeholder="Original" min="0">
</div> </div>
<div> <input type="range" class="scale-slider" id="scale-slider" min="10" max="100" value="100" step="5">
<label style="font-weight: normal;">Height</label> <div class="resolution-preview" id="resolution-preview">
<input type="number" id="height" placeholder="Original" min="0"> <p style="margin: 0;">💡 <strong>How it works:</strong> Scale is applied to the cropped area (or original if no crop). At 50%, a 200x400 crop becomes 100x200.</p>
</div>
<div class="preset-resolutions" style="margin-top: 15px;">
<button class="preset-btn" data-scale="100">100% (Original)</button>
<button class="preset-btn" data-scale="75">75%</button>
<button class="preset-btn" data-scale="50">50%</button>
<button class="preset-btn" data-scale="25">25%</button>
</div> </div>
</div> </div>
</div> </div>
@@ -595,6 +651,7 @@
let isDraggingEnd = false; let isDraggingEnd = false;
let timelineStartPos = 0; let timelineStartPos = 0;
let timelineEndPos = 100; let timelineEndPos = 100;
let scalePercentage = 100;
// Upload area handling // Upload area handling
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
@@ -674,6 +731,9 @@
// Set default values // Set default values
document.getElementById('end-time').value = videoInfo.duration.toFixed(2); document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
scalePercentage = 100;
scaleSlider.value = 100;
updateScaleDisplay();
// Setup canvas for cropping // Setup canvas for cropping
setupCropCanvas(); setupCropCanvas();
@@ -915,26 +975,47 @@
updateTimeline(); updateTimeline();
}); });
// Resolution presets // Scale slider
const scaleSlider = document.getElementById('scale-slider');
const scaleValueDisplay = document.getElementById('scale-value-display');
const outputResolution = document.getElementById('output-resolution');
function updateScaleDisplay() {
scalePercentage = parseInt(scaleSlider.value);
scaleValueDisplay.textContent = scalePercentage + '%';
// Calculate output resolution
const cropWidth = parseInt(document.getElementById('crop-width').value) || videoInfo.width;
const cropHeight = parseInt(document.getElementById('crop-height').value) || videoInfo.height;
const outputWidth = Math.round((cropWidth * scalePercentage) / 100);
const outputHeight = Math.round((cropHeight * scalePercentage) / 100);
outputResolution.textContent = `${outputWidth}x${outputHeight}`;
}
scaleSlider.addEventListener('input', updateScaleDisplay);
// Scale presets
document.querySelectorAll('.preset-btn').forEach(btn => { document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active')); const scale = btn.dataset.scale;
btn.classList.add('active'); if (scale) {
scaleSlider.value = scale;
const width = btn.dataset.width; updateScaleDisplay();
const height = btn.dataset.height; }
document.getElementById('width').value = width === '0' ? '' : width;
document.getElementById('height').value = height === '0' ? '' : height;
}); });
}); });
// Update scale display when crop values change
['crop-width', 'crop-height'].forEach(id => {
document.getElementById(id).addEventListener('input', updateScaleDisplay);
});
// Process video // Process video
document.getElementById('process-btn').addEventListener('click', async () => { document.getElementById('process-btn').addEventListener('click', async () => {
const startTime = parseFloat(document.getElementById('start-time').value) || 0; const startTime = parseFloat(document.getElementById('start-time').value) || 0;
const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration; const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration;
const width = parseInt(document.getElementById('width').value) || 0;
const height = parseInt(document.getElementById('height').value) || 0;
const cropX = parseInt(document.getElementById('crop-x').value) || 0; const cropX = parseInt(document.getElementById('crop-x').value) || 0;
const cropY = parseInt(document.getElementById('crop-y').value) || 0; const cropY = parseInt(document.getElementById('crop-y').value) || 0;
const cropWidth = parseInt(document.getElementById('crop-width').value) || 0; const cropWidth = parseInt(document.getElementById('crop-width').value) || 0;
@@ -943,13 +1024,10 @@
const payload = { const payload = {
file_id: currentFileId, file_id: currentFileId,
start_time: startTime, start_time: startTime,
end_time: endTime end_time: endTime,
scale_percentage: scalePercentage
}; };
if (width > 0 && height > 0) {
payload.resolution = { width, height };
}
if (cropWidth > 0 && cropHeight > 0) { if (cropWidth > 0 && cropHeight > 0) {
payload.crop = { payload.crop = {
x: cropX, x: cropX,