This commit is contained in:
Concept Agent
2026-05-15 18:02:38 +02:00
parent 72016728e2
commit 307e452251
175 changed files with 9316 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lahr Clip Browser</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0a0a0b; color: #eee; font-family: Inter, sans-serif; padding: 24px; }
h1 { font-size: 20px; color: #e8291b; margin-bottom: 6px; }
p.sub { color: #888; font-size: 13px; margin-bottom: 24px; }
.layout { display: grid; grid-template-columns: 1fr 340px; gap: 24px; }
#clip-list { display: flex; flex-direction: column; gap: 10px; }
.clip-item {
display: flex; align-items: center; gap: 12px;
background: #161618; border: 1px solid #2a2a2e; border-radius: 8px;
padding: 10px 12px; cursor: grab; user-select: none;
transition: border-color 0.15s;
}
.clip-item:hover { border-color: #e8291b; }
.clip-item.dragging { opacity: 0.4; }
.clip-item.drag-over { border-color: #e8291b; background: #1f1012; }
.drag-handle { color: #555; font-size: 18px; flex-shrink: 0; cursor: grab; }
.clip-thumb { width: 120px; height: 68px; object-fit: cover; border-radius: 4px; flex-shrink: 0; background: #222; }
.clip-info { flex: 1; min-width: 0; }
.clip-name { font-size: 13px; font-weight: 600; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clip-size { font-size: 11px; color: #666; margin-top: 2px; }
.clip-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.btn-preview { background: #1e1e22; border: 1px solid #333; color: #ccc; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
.btn-preview:hover { border-color: #e8291b; color: #e8291b; }
.btn-remove { background: transparent; border: 1px solid #333; color: #555; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
.btn-remove:hover { border-color: #e8291b; color: #e8291b; }
.sidebar { position: sticky; top: 24px; }
.preview-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.preview-box h3 { font-size: 13px; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
#preview-video { width: 100%; border-radius: 6px; background: #000; }
#preview-name { font-size: 12px; color: #666; margin-top: 8px; text-align: center; }
.reel-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; }
.reel-box h3 { font-size: 13px; color: #888; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
#reel-order { font-size: 11px; color: #666; line-height: 1.8; margin-bottom: 14px; max-height: 220px; overflow-y: auto; }
.btn-build { width: 100%; background: #e8291b; color: #fff; border: none; padding: 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-build:hover { background: #c72216; }
#build-output { margin-top: 12px; font-size: 11px; color: #888; background: #0d0d0f; border-radius: 4px; padding: 10px; display: none; white-space: pre-wrap; }
.unused-section { margin-top: 32px; }
.unused-section h2 { font-size: 14px; color: #555; margin-bottom: 12px; }
#unused-list { display: flex; flex-direction: column; gap: 8px; }
.unused-item { display: flex; align-items: center; gap: 12px; background: #111; border: 1px solid #1e1e22; border-radius: 8px; padding: 8px 12px; }
.unused-item .clip-name { font-size: 12px; color: #666; }
.btn-add { background: #1e1e22; border: 1px solid #333; color: #888; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; margin-left: auto; }
.btn-add:hover { border-color: #4a9; color: #4a9; }
</style>
</head>
<body>
<h1>Lahr Clip Browser</h1>
<p class="sub">Drag clips to reorder. Click Preview to watch. Remove clips from reel. Build Reel when ready.</p>
<div class="layout">
<div>
<div id="clip-list"></div>
<div class="unused-section">
<h2>Available (not in reel)</h2>
<div id="unused-list"></div>
</div>
</div>
<div class="sidebar">
<div class="preview-box">
<h3>Preview</h3>
<video id="preview-video" controls></video>
<div id="preview-name">Click Preview on any clip</div>
</div>
<div class="reel-box">
<h3>Current Reel Order</h3>
<div id="reel-order"></div>
<button class="btn-build" onclick="buildReel()">Build Reel</button>
<div id="build-output"></div>
</div>
</div>
</div>
<script>
const BASE = '/assets/videos/hero/clips/';
const ALL_CLIPS = [
{ name: 'v3-shot-01', label: 'v3 · Family enters door, pans to carpet', size: '5.2MB' },
{ name: 'v3-shot-02', label: 'v3 · Wine spill on sofa close-up', size: '6.0MB' },
{ name: 'v3-shot-03', label: 'v3 · Dirty stained carpet close-up', size: '3.3MB' },
{ name: 'v3-shot-04', label: 'v3 · Clean bright sofa pullback', size: '5.6MB' },
{ name: 'v3-shot-05', label: 'v3 · Office lobby carpet pan', size: '4.7MB' },
{ name: 'v3-shot-06', label: 'v3 · Living room clean carpet pan', size: '4.9MB' },
{ name: 'v3-shot-07', label: 'v3 · Restaurant carpet glide', size: '3.1MB' },
{ name: 'v2-shot-01-door-entry', label: 'v2 · Door entry muddy boots', size: '2.2MB' },
{ name: 'v2-shot-02-mud-on-carpet', label: 'v2 · Mud boots on carpet floor level', size: '4.1MB' },
{ name: 'v2-shot-03-stain-on-chair', label: 'v2 · Stain on chair close-up', size: '3.3MB' },
{ name: 'v2-shot-04-extraction-carpet', label: 'v2 · Extraction machine on carpet', size: '4.2MB' },
{ name: 'v2-shot-05-clean-stairs', label: 'v2 · Clean bright staircase', size: '5.6MB' },
{ name: 'v2-shot-06-office', label: 'v2 · Bright office carpet', size: '4.7MB' },
{ name: 'v2-shot-07-restaurant', label: 'v2 · Restaurant carpet', size: '4.7MB' },
{ name: 'shot-01-door-opens-trimmed', label: 'v1 · Door opens (trimmed 2.5s)', size: '525KB' },
{ name: 'shot-01-wide-room', label: 'v1 · Wide room establishing', size: '2.6MB' },
{ name: 'shot-02-pan-to-stains', label: 'v1 · Pan to stains / muddy shoes', size: '2.8MB' },
{ name: 'shot-02-staircase', label: 'v1 · Staircase', size: '1.4MB' },
{ name: 'shot-03-stain-closeup', label: 'v1 · Stain close-up', size: '4.7MB' },
{ name: 'shot-03-technician', label: 'v1 · Technician', size: '1.6MB' },
{ name: 'shot-04-extraction-carpet', label: 'v1 · Extraction carpet (clean reveal)', size: '6.2MB' },
{ name: 'shot-04-extraction-closeup', label: 'v1 · Extraction close-up', size: '2.0MB' },
{ name: 'shot-05-clean-reveal', label: 'v1 · Clean reveal', size: '2.2MB' },
{ name: 'shot-05-extraction-couch', label: 'v1 · Extraction couch', size: '3.5MB' },
{ name: 'shot-06-extraction-stairs', label: 'v1 · Extraction stairs', size: '5.4MB' },
{ name: 'shot-07-office-entryway', label: 'v1 · Office entryway', size: '5.9MB' },
{ name: 'shot-08-showroom', label: 'v1 · Showroom', size: '4.1MB' },
{ name: 'shot-09-technician-unloading', label: 'v1 · Technician unloading van', size: '3.7MB' },
{ name: 'shot-01-door-opens', label: 'v1 · Door opens (full 6s)', size: '1.5MB' },
];
// Default reel = current v3 set
let reelClips = ALL_CLIPS.slice(0, 7).map(c => c.name);
function getClip(name) { return ALL_CLIPS.find(c => c.name === name); }
function render() {
const list = document.getElementById('clip-list');
list.innerHTML = '';
reelClips.forEach((name, idx) => {
const clip = getClip(name);
if (!clip) return;
const div = document.createElement('div');
div.className = 'clip-item';
div.draggable = true;
div.dataset.name = name;
div.innerHTML = `
<span class="drag-handle">⠿</span>
<video class="clip-thumb" src="${BASE}${name}.mp4" muted preload="metadata"></video>
<div class="clip-info">
<div class="clip-name">${idx+1}. ${clip.label}</div>
<div class="clip-size">${clip.size}</div>
</div>
<div class="clip-actions">
<button class="btn-preview" onclick="preview('${name}', '${clip.label}')">Preview</button>
<button class="btn-remove" onclick="remove('${name}')">Remove</button>
</div>`;
div.addEventListener('dragstart', dragStart);
div.addEventListener('dragover', dragOver);
div.addEventListener('drop', drop);
div.addEventListener('dragend', dragEnd);
list.appendChild(div);
});
const unused = ALL_CLIPS.filter(c => !reelClips.includes(c.name));
const ulist = document.getElementById('unused-list');
ulist.innerHTML = '';
unused.forEach(clip => {
const div = document.createElement('div');
div.className = 'unused-item';
div.innerHTML = `
<div class="clip-name">${clip.label} <span style="color:#444">(${clip.size})</span></div>
<button class="btn-add" onclick="addToReel('${clip.name}')">+ Add</button>`;
ulist.appendChild(div);
});
const orderEl = document.getElementById('reel-order');
orderEl.innerHTML = reelClips.map((n,i) => {
const c = getClip(n);
return `<div>${i+1}. ${c ? c.label : n}</div>`;
}).join('');
}
function preview(name, label) {
const v = document.getElementById('preview-video');
v.src = BASE + name + '.mp4';
v.play();
document.getElementById('preview-name').textContent = label;
}
function remove(name) {
reelClips = reelClips.filter(n => n !== name);
render();
}
function addToReel(name) {
reelClips.push(name);
render();
}
let dragSrc = null;
function dragStart(e) { dragSrc = this; this.classList.add('dragging'); }
function dragOver(e) { e.preventDefault(); document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('drag-over')); this.classList.add('drag-over'); }
function drop(e) {
e.preventDefault();
if (dragSrc === this) return;
const fromName = dragSrc.dataset.name;
const toName = this.dataset.name;
const fi = reelClips.indexOf(fromName);
const ti = reelClips.indexOf(toName);
reelClips.splice(fi, 1);
reelClips.splice(ti, 0, fromName);
render();
}
function dragEnd() { document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('dragging','drag-over')); }
async function buildReel() {
const out = document.getElementById('build-output');
out.style.display = 'block';
out.textContent = 'Sending to server...';
const resp = await fetch('/tools/build-reel-api.py', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clips: reelClips })
});
if (!resp.ok) {
// Fallback: show the ffmpeg command to copy
const lines = reelClips.map(n => `file 'assets/videos/hero/clips/${n}.mp4'`).join('\n');
out.textContent = 'Server not available. Run this manually:\n\n' +
'# 1. Save as concat.txt:\n' + lines + '\n\n' +
'# 2. Run ffmpeg:\n' +
'ffmpeg -y -f concat -safe 0 -i concat.txt -c:v libx264 -crf 22 -preset fast -movflags +faststart assets/videos/hero/hero-reel.mp4';
} else {
const data = await resp.json();
out.textContent = data.message || 'Done.';
}
}
render();
</script>
</body>
</html>