:root{ --bg:#050705; --panel:#0a100b; --panel2:#030603; --line:#174524; --green:#42ff72; --text:#eaffee; --muted:#7da488; --red:#ff5c6a; --amber:#ffc857; } *{box-sizing:border-box} body{ margin:0;background:radial-gradient(circle at top,#0b160d 0,#050705 45%,#020302 100%); color:var(--text);font-family:Arial,"Noto Sans Myanmar",sans-serif; } header{ position:sticky;top:0;z-index:5;background:rgba(4,8,5,.94);backdrop-filter:blur(10px); border-bottom:1px solid var(--line);padding:16px; } header h1{margin:0;color:var(--green);font-size:21px;letter-spacing:1px} header p{margin:5px 0 0;color:var(--muted);font-size:12px} .wrap{width:min(1100px,94%);margin:18px auto 45px} .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px} .card{ background:linear-gradient(180deg,#0b120d,#070b08);border:1px solid var(--line); border-radius:16px;padding:16px;box-shadow:0 12px 40px rgba(0,0,0,.28); } .card.full{grid-column:1/-1} .label{font-size:12px;color:var(--green);font-weight:700;letter-spacing:.7px;margin-bottom:10px} input[type=file]{width:100%;padding:12px;border:1px dashed #28683b;border-radius:12px;background:#030603;color:#bfe9c9} video{width:100%;max-height:360px;background:#000;border-radius:12px;border:1px solid #16361f;margin-top:12px} .meta{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px} .meta div{background:#061009;border:1px solid #163e20;border-radius:10px;padding:9px;font-size:12px;color:#a8d6b3} .meta b{display:block;color:#fff;margin-top:3px;font-size:13px} textarea{ width:100%;min-height:300px;background:var(--panel2);color:#dffff0;border:1px solid #1a5029; border-radius:12px;padding:13px;resize:vertical;outline:none;font:13px/1.65 Consolas,monospace; } textarea:focus{border-color:var(--green);box-shadow:0 0 0 2px rgba(66,255,114,.08)} .row{display:flex;gap:9px;flex-wrap:wrap;margin-top:12px} button,a.download{ border:0;border-radius:10px;padding:11px 15px;font-weight:700;font-size:13px;cursor:pointer;text-decoration:none; } .primary{background:var(--green);color:#031006} .secondary{background:#102718;color:#8dffa9;border:1px solid #235d34} .danger{background:#2c1114;color:#ff7e88;border:1px solid #6c2730} button:disabled{opacity:.42;cursor:not-allowed} .status{ margin-top:12px;padding:10px 12px;border-radius:10px;background:#061009;border:1px solid #173e22; color:#a4ccb0;font-size:12px;white-space:pre-wrap } .ok{color:#66ff8e}.bad{color:#ff7883}.warn{color:var(--amber)} .progress{height:9px;background:#071108;border:1px solid #173e21;border-radius:999px;overflow:hidden;margin-top:10px} .bar{height:100%;width:0;background:linear-gradient(90deg,#2be65a,#6cff92);transition:width .15s} .tableWrap{overflow:auto;max-height:360px;border:1px solid #163e20;border-radius:12px} table{width:100%;border-collapse:collapse;font-size:12px;min-width:720px} th,td{padding:9px;border-bottom:1px solid #12351c;text-align:left} th{position:sticky;top:0;background:#0d1b10;color:#7ffb9b} code{color:#d9ffe2} .small{font-size:11px;color:#799a81;margin-top:8px;line-height:1.5} #cmd{min-height:170px} @media(max-width:760px){ .grid{grid-template-columns:1fr}.meta{grid-template-columns:1fr 1fr}.card.full{grid-column:auto} }
SILENT RECAP EDITOR
Video Index → @EDIT Short Code → Browser FFmpeg → Silent MP4
01 — VIDEO INDEX
FILE—
DURATION—
SIZE—
RATIO—
Video ကို server သို့ upload မလုပ်ပါ။ Browser memory ထဲမှာ FFmpeg.wasm က process လုပ်မယ်။
02 — EDIT PLAN INDEX
INDEX PLAN CLEAR
Waiting for edit plan…
03 — PARSED CLIPS
#IDUSERAW DUR.SPEEDZOOMREPEATFREEZEFADENo indexed clips.
04 — FFMPEG ENGINE
LOAD FFMPEG RENDER SILENT VIDEO
FFmpeg not loaded.
ZF = face detection မဟုတ်သေးပါ။ ဒီ prototype မှာ center zoom အဖြစ်သုံးမယ်။ Output မှာ audio track မပါ။
05 — OUTPUT
SAVE MP4
DEBUG — GENERATED FLOW
import { FFmpeg } from "https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.15/dist/esm/index.js"; import { fetchFile, toBlobURL } from "https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.2/dist/esm/index.js"; const $ = id => document.getElementById(id); let selectedFile = null; let parsedClips = []; let ffmpeg = null; let ffmpegReady = false; let sourceObjectURL = ""; let outputObjectURL = ""; const supported = new Set(["SP","SL","Z","ZF","CU","RP","FR","FD"]); function fmtSec(n){ if(!Number.isFinite(n)) return "—"; const m = Math.floor(n/60); const s = (n%60).toFixed(3).padStart(6,"0"); return `${String(m).padStart(2,"0")}:${s}`; } function gcd(a,b){ a=Math.round(a);b=Math.round(b); while(b){const t=b;b=a%b;a=t} return a||1; } function humanSize(bytes){ const u=["B","KB","MB","GB"]; let i=0,n=bytes; while(n>=1024 && i{ const f=e.target.files?.[0]; if(!f) return; selectedFile=f; if(sourceObjectURL) URL.revokeObjectURL(sourceObjectURL); sourceObjectURL=URL.createObjectURL(f); $("sourcePreview").src=sourceObjectURL; $("mName").textContent=f.name; $("mSize").textContent=humanSize(f.size); $("sourcePreview").onloadedmetadata=()=>{ const v=$("sourcePreview"); $("mDur").textContent=fmtSec(v.duration); const g=gcd(v.videoWidth,v.videoHeight); $("mRatio").textContent=`${v.videoWidth/g}:${v.videoHeight/g} (${v.videoWidth}×${v.videoHeight})`; updateRenderButton(); }; }); function extractEditBlock(raw){ raw=(raw||"").trim(); if(!raw) throw new Error("Edit plan မရှိသေးပါ။"); if(raw.includes("@EDIT")){ raw=raw.split("@EDIT")[1] || ""; } // Gemini က edit lines အားလုံးကို တစ်ကြောင်းတည်းထုတ်လာရင် line-break ပြန်ခွဲရန်။ raw=raw.replace(/\s+(?=\d+[A-Za-z]?\|)/g,"\n").trim(); return raw; } function parseEffects(parts){ const fx={speed:1,zoom:1,repeat:1,freeze:0,fade:0,raw:[]}; let hasSP=false,hasSL=false; for(const token0 of parts){ const token=token0.trim(); if(!token) continue; const m=token.match(/^([A-Za-z]+)\s*=\s*([0-9.]+)$/); if(!m) throw new Error(`Effect format မမှန်: ${token}`); const code=m[1].toUpperCase(); const val=Number(m[2]); if(!supported.has(code)) throw new Error(`Unsupported code: ${code}`); if(!Number.isFinite(val)) throw new Error(`Invalid value: ${token}`); fx.raw.push(`${code}=${val}`); if(code==="SP"){ if(hasSL) throw new Error("SP နဲ့ SL ကို clip တစ်ခုတည်းမှာ မသုံးပါနဲ့။"); hasSP=true; if(val<=0 || val>4) throw new Error(`SP out of range: ${val}`); fx.speed=val; } if(code==="SL"){ if(hasSP) throw new Error("SP နဲ့ SL ကို clip တစ်ခုတည်းမှာ မသုံးပါနဲ့။"); hasSL=true; if(val<=0 || val>=1) throw new Error(`SL must be 0–1: ${val}`); fx.speed=val; } if(["Z","ZF","CU"].includes(code)){ if(val<1 || val>3) throw new Error(`${code} out of range: ${val}`); fx.zoom=Math.max(fx.zoom,val); } if(code==="RP"){ fx.repeat=Math.max(1,Math.min(5,Math.round(val))); } if(code==="FR"){ fx.freeze=Math.max(0,Math.min(5,val)); } if(code==="FD"){ fx.fade=Math.max(0,Math.min(2,val)); } } return fx; } function parsePlan(){ const raw=extractEditBlock($("plan").value); const lines=raw.split(/\n+/).map(s=>s.trim()).filter(Boolean); if(!lines.length) throw new Error("Edit lines မတွေ့ပါ။"); const out=[]; for(const line of lines){ const p=line.split("|").map(s=>s.trim()); if(p.length<3) throw new Error(`Line မပြည့်စုံ: ${line}`); const id=p[0]; if(!/^\d+[A-Za-z]?$/.test(id)) throw new Error(`ID မမှန်: ${id}`); const src=p[1].split(",").map(Number); const use=p[2].split(",").map(Number); if(src.length!==2 || use.length!==2 || src.some(Number.isNaN) || use.some(Number.isNaN)){ throw new Error(`Timestamp format မမှန်: ${line}`); } const [sourceStart,sourceEnd]=src; const [useStart,useEnd]=use; if(!(sourceStart<=useStart && useStartc.useEnd>videoDur+0.05); if(bad) throw new Error(`${bad.id} timestamp (${bad.useEnd}s) က video duration ထက်ကျော်နေပါတယ်။`); } parsedClips=out; drawClips(); buildFlowPreview(); $("parseStatus").innerHTML=`Indexed ${out.length} clips successfully.`; updateRenderButton(); } function drawClips(){ const tb=$("clipRows"); if(!parsedClips.length){ tb.innerHTML='No indexed clips. '; return; } tb.innerHTML=parsedClips.map((c,i)=>` ${i+1} ${c.id} ${c.useStart.toFixed(3)} → ${c.useEnd.toFixed(3)} ${c.rawDuration.toFixed(2)}s ${c.speed.toFixed(2)}x ${c.zoom.toFixed(2)}x ${c.repeat}× ${c.freeze.toFixed(2)}s ${c.fade.toFixed(2)}s `).join(""); } function makeVF(c){ const vf=[]; if(Math.abs(c.speed-1)>0.0001){ // SP 1.2 => faster. SL 0.8 => slower. vf.push(`setpts=PTS/${c.speed}`); } if(c.zoom>1.0001){ const z=c.zoom.toFixed(4); // Center crop + scale back, keeps the source output ratio. vf.push(`crop=iw/${z}:ih/${z}:(iw-iw/${z})/2:(ih-ih/${z})/2`); vf.push(`scale=trunc(iw*${z}/2)*2:trunc(ih*${z}/2)*2`); } // libx264 + yuv420p needs even dimensions. vf.push("scale=trunc(iw/2)*2:trunc(ih/2)*2"); if(c.freeze>0){ vf.push(`tpad=stop_mode=clone:stop_duration=${c.freeze.toFixed(3)}`); } if(c.fade>0){ const d=Math.min(c.fade,Math.max(0.05,c.playedDuration/2)); const outStart=Math.max(0,c.playedDuration-d); vf.push(`fade=t=in:st=0:d=${d.toFixed(3)}`); vf.push(`fade=t=out:st=${outStart.toFixed(3)}:d=${d.toFixed(3)}`); } return vf.join(","); } function buildFlowPreview(){ if(!parsedClips.length){ $("cmd").value=""; return; } const lines=[]; parsedClips.forEach((c,i)=>{ const vf=makeVF(c); lines.push( `[${i+1}] ${c.id} USE ${c.useStart.toFixed(3)} → ${c.useEnd.toFixed(3)} FX ${c.raw.length ? c.raw.join(" | ") : "NONE"} FFmpeg flow: -ss ${c.useStart.toFixed(3)} -t ${c.rawDuration.toFixed(3)} -i input -an -vf "${vf}" -c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p clip_${String(i).padStart(3,"0")}.mp4` ); }); lines.push("\nFINAL: concat all processed clips in plan order → audio removed → final.mp4"); $("cmd").value=lines.join("\n\n"); } $("parseBtn").addEventListener("click",()=>{ try{ parsePlan(); } catch(err){ parsedClips=[]; drawClips(); $("parseStatus").innerHTML=`${escapeHtml(err.message)}`; updateRenderButton(); } }); $("clearBtn").addEventListener("click",()=>{ $("plan").value=""; parsedClips=[]; drawClips(); $("cmd").value=""; $("parseStatus").textContent="Waiting for edit plan…"; $("bar").style.width="0%"; updateRenderButton(); }); function escapeHtml(s){ return String(s).replace(/[&<>"']/g,m=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[m])); } function setProgress(p,text){ $("bar").style.width=`${Math.max(0,Math.min(100,p))}%`; if(text) $("renderStatus").textContent=text; } function updateRenderButton(){ $("renderBtn").disabled=!(ffmpegReady && selectedFile && parsedClips.length); } async function makeClassWorkerURL(){ // Avoid a cross-origin Worker URL by creating a same-origin blob module // that imports the official ffmpeg worker. const workerModule="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.15/dist/esm/worker.js"; const blob=new Blob([`import "${workerModule}";`],{type:"text/javascript"}); return URL.createObjectURL(blob); } $("loadBtn").addEventListener("click", async()=>{ if(ffmpegReady) return; $("loadBtn").disabled=true; setProgress(2,"Loading FFmpeg core… first load is roughly 30 MB."); try{ ffmpeg=new FFmpeg(); ffmpeg.on("log",({message})=>{ console.log("[ffmpeg]",message); }); ffmpeg.on("progress",({progress})=>{ // This event is experimental; clip-level progress below remains authoritative. if(Number.isFinite(progress)){ const current=parseFloat($("bar").style.width)||0; const bump=Math.min(3,Math.max(0,progress*3)); $("bar").style.width=`${Math.min(99,current+bump)}%`; } }); const baseURL="https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd"; const classWorkerURL=await makeClassWorkerURL(); await ffmpeg.load({ classWorkerURL, coreURL:await toBlobURL(`${baseURL}/ffmpeg-core.js`,"text/javascript"), wasmURL:await toBlobURL(`${baseURL}/ffmpeg-core.wasm`,"application/wasm") }); ffmpegReady=true; setProgress(0,"FFmpeg loaded. Ready to render."); $("loadBtn").textContent="FFMPEG READY"; $("renderStatus").innerHTML='FFmpeg loaded successfully.'; updateRenderButton(); }catch(err){ console.error(err); $("renderStatus").innerHTML= `FFmpeg load failed.\n${escapeHtml(err.message||err)}\n\nIf Blogger blocks workers/WASM, test this same HTML from normal HTTPS hosting or localhost.`; $("loadBtn").disabled=false; } }); async function safeDelete(path){ try{ await ffmpeg.deleteFile(path); }catch(e){} } $("renderBtn").addEventListener("click", async()=>{ if(!selectedFile || !parsedClips.length || !ffmpegReady) return; $("renderBtn").disabled=true; $("loadBtn").disabled=true; $("downloadBtn").style.display="none"; $("outputPreview").removeAttribute("src"); if(outputObjectURL){ URL.revokeObjectURL(outputObjectURL); outputObjectURL=""; } const inputName="source_input"+(selectedFile.name.match(/\.[A-Za-z0-9]+$/)?.[0] || ".mp4"); try{ setProgress(1,"Copying video into FFmpeg memory…"); await safeDelete(inputName); await ffmpeg.writeFile(inputName,await fetchFile(selectedFile)); const concatEntries=[]; const total=parsedClips.length; for(let i=0;iSUCCESS — Video-only render complete. Audio track was removed.'; }catch(err){ console.error(err); $("renderStatus").innerHTML=`RENDER FAILED\n${escapeHtml(err.message||err)}`; }finally{ $("renderBtn").disabled=false; $("loadBtn").disabled=ffmpegReady; updateRenderButton(); } }); // Save plan between refreshes. $("plan").value=localStorage.getItem("silent_recap_plan")||""; $("plan").addEventListener("input",()=>localStorage.setItem("silent_recap_plan",$("plan").value));