wheelie-app / create.html
Wayed11's picture
Wheelie character lock in talking clips
f5032e0 verified
Raw History Blame Contribute Delete
45.3 kB
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="apple-mobile-web-app-capable" content="yes"><title>Wheelie Create</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{background:#010101;color:#fefefe;font-family:-apple-system,system-ui,sans-serif;height:100%}
header{position:fixed;top:0;left:0;right:0;z-index:5;padding:calc(env(safe-area-inset-top) + 10px) 16px 10px;display:flex;align-items:center;gap:12px;background:linear-gradient(#010101,rgba(1,1,1,0))}
header a{color:#fefefe;text-decoration:none;font-size:26px;line-height:1}
header h1{font-size:20px;font-weight:800;letter-spacing:.5px}
header h1 b{color:#FF6FB5}
.tabs{margin-left:auto;display:flex;background:#1a1a1a;border-radius:20px;padding:3px}
.tabs button{border:0;background:none;color:#aaa;font-weight:700;padding:7px 14px;border-radius:17px;font-size:14px}
.tabs button.on{background:#FF6FB5;color:#010101}
#feed{padding:calc(env(safe-area-inset-top) + 70px) 10px 200px;display:grid;grid-template-columns:1fr 1fr;gap:8px}
.card{position:relative;border-radius:16px;overflow:hidden;background:#111;aspect-ratio:9/16}
.card video,.card img{width:100%;height:100%;object-fit:cover;display:block}
.card .busy{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;font-size:13px;color:#ccc;text-align:center;padding:10px}
.ring{width:42px;height:42px;border-radius:50%;border:4px solid #222;border-top-color:#FF6FB5;border-right-color:#5BC0F8;animation:s 1s linear infinite}
@keyframes s{to{transform:rotate(360deg)}}
.empty{grid-column:1/3;text-align:center;color:#888;padding:80px 20px;font-size:16px;line-height:1.5}
.empty b{display:block;font-size:26px;color:#fefefe;margin-bottom:8px}
#full{position:fixed;inset:0;background:#010101;z-index:20;display:none;align-items:center;justify-content:center}
#full video{width:100%;height:100%;object-fit:contain}
#full .x{position:absolute;top:calc(env(safe-area-inset-top) + 12px);right:16px;font-size:30px;background:none;border:0;color:#fefefe;z-index:2}
#full .act{position:absolute;bottom:calc(env(safe-area-inset-bottom) + 20px);left:0;right:0;display:flex;justify-content:center;gap:12px;z-index:2}
#full .act button{border:0;border-radius:24px;padding:12px 20px;font-weight:800;font-size:15px;background:#FF6FB5;color:#010101}
#full .act button.b{background:#5BC0F8}
#bar{position:fixed;left:0;right:0;bottom:0;z-index:10;padding:10px 12px calc(env(safe-area-inset-bottom) + 12px);background:linear-gradient(rgba(1,1,1,0),#010101 30%)}
.modes{display:flex;gap:8px;margin-bottom:10px;overflow-x:auto}
.modes button{border:1px solid #333;background:#111;color:#ddd;border-radius:16px;padding:7px 14px;font-weight:700;font-size:13px;white-space:nowrap}
.modes button.on{border-color:#5BC0F8;color:#5BC0F8}
.box{display:flex;align-items:flex-end;gap:8px;background:#161616;border:1px solid #2a2a2a;border-radius:26px;padding:8px}
.box textarea{flex:1;background:none;border:0;color:#fefefe;font-size:17px;resize:none;outline:none;max-height:120px;padding:8px 4px;font-family:inherit}
.ib{width:42px;height:42px;border-radius:50%;border:0;background:#262626;color:#fefefe;font-size:20px;flex:none;display:flex;align-items:center;justify-content:center}
.ib.go{background:linear-gradient(90deg,#FF6FB5 50%,#5BC0F8 50%);color:#010101;font-weight:900}
.ib.rec{background:#FF6FB5}
#thumb{display:none;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;color:#ccc}
#thumb img{width:44px;height:44px;border-radius:10px;object-fit:cover}
#thumb button{background:none;border:0;color:#FF6FB5;font-weight:800}
</style></head><body>
<header><a href="./" aria-label="Back to Wheelie">‹</a><h1>Wheelie <b>Create</b></h1>
<div class="tabs"><button id="tV" class="on">Video</button><button id="tW">9:16</button></div></header>
<main id="feed"><div class="empty"><b>Make something.</b>Type or talk your idea. Add a picture to bring it to life. Every video comes with sound.</div></main>
<div id="full"><button class="x" aria-label="Close">×</button><video playsinline controls></video><div class="act"><button id="ext">Keep going</button><button class="b" id="sv">Save</button></div></div>
<div id="bar"><div id="thumb"><img alt=""><span>Starting from this picture</span><button id="rmImg">Remove</button></div>
<div class="modes"><button class="on" data-m="normal">Normal</button><button data-m="fun">Fun</button><button data-m="cine">Cinematic</button><button data-m="custom">Custom</button></div>
<div class="box"><button class="ib" id="pic" aria-label="Add picture">+</button><input type="file" id="file" accept="image/*" hidden>
<textarea id="p" rows="1" placeholder="What should Wheelie make?"></textarea>
<button class="ib" id="mic" aria-label="Talk">🎤</button><button class="ib go" id="go" aria-label="Make it">↑</button></div></div>
<script>
// Wheelie Create engine v5: Qwen still from her real Grok pictures (body + face) -> talking (LTX-2.5) or Wan 2.2 + MMAudio.
const SB='https://yzzxvjrzniuhxebqqrsy.supabase.co',GEN=SB+'/functions/v1/gen-call',API=SB+'/functions/v1/wheelie-api',PUB=SB+'/storage/v1/object/public/public/';
const QWEN='https://qwen-qwen-image-edit-2511.hf.space',WAN='https://zerogpu-aoti-wan2-2-fp8da-aoti-faster.hf.space',MMA='https://hkchengrex-mmaudio.hf.space',MIX='https://wayed11-wheelie-mix.hf.space',FLUX='https://wayed11-wheelie-image.hf.space';
const REF=SB+'/storage/v1/object/public/public/gen/ref/body.webp',S2V='https://wan-ai-wan2-2-s2v.hf.space',LTX='https://lightricks-ltx-2-5.hf.space';
const BODY=PUB+'gen/ref/body.webp',FACE=PUB+'gen/ref/face.webp';
const LOOK='The same young woman from image 1 and image 2, exactly the same character: her exact face from image 2 (freckles, small beauty mark under her eye, thick dark brows, heavy-lidded brown eyes with winged liner, full lips, curly dark brown messy high bun, small silver hoops), her exact rainbow striped chunky knit sweater, dark jeans, purple high-top sneakers, and her exact black power wheelchair with rainbow LED wheel lights and a coffee cup in the cup holder from image 1. She drives it herself.';
const LOOKS='Same stylized 3D animated film look, lighting quality and rendering style as the reference images. No text, no watermark.';
const VNEG='watermark, logo, captions, text, teenager, child, crop top, bare midriff, crying, static frame, blurry, jitter, morphing face, extra fingers, a person pushing the wheelchair';
const MODES={normal:' Smooth natural motion.',fun:' Playful, bouncy, funny comic timing, energetic camera.',cine:' Epic slow cinematic camera move, dramatic lighting.',custom:''};
let mode='normal',img=null,useHer=true,wide=false,last=null,lastText='';
const $=s=>document.querySelector(s),feed=$('#feed');
const FD=(p,u)=>({path:p,url:u||(p.startsWith('http')?p:undefined),meta:{_type:'gradio.FileData'}});
document.querySelector('.empty').innerHTML='<b>Make something.</b>Type or talk your idea. Wheelie stars by default. Put her line in quotes and she says it. Every video comes with sound.';
function showThumb(){const t=$('#thumb');if(img){$('#thumb img').src=URL.createObjectURL(img);$('#thumb span').textContent='Starting from your picture';t.style.display='flex'}else if(useHer){$('#thumb img').src=REF;$('#thumb span').textContent='Starring Wheelie';t.style.display='flex'}else t.style.display='none'}
showThumb();
document.querySelectorAll('.modes button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.modes button').forEach(x=>x.classList.remove('on'));b.classList.add('on');mode=b.dataset.m});
$('#tW').onclick=()=>{wide=!wide;$('#tW').textContent=wide?'16:9':'9:16'};
$('#pic').onclick=()=>$('#file').click();
$('#file').onchange=e=>{const f=e.target.files[0];if(!f)return;img=f;showThumb()};
$('#rmImg').onclick=()=>{if(img){img=null;$('#file').value=''}else useHer=false;showThumb()};
const p=$('#p');p.oninput=()=>{p.style.height='auto';p.style.height=p.scrollHeight+'px'};
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;let rec=null;
$('#mic').onclick=()=>{if(!SR){p.focus();return}if(rec){rec.stop();return}rec=new SR();rec.lang='en-US';rec.interimResults=true;$('#mic').classList.add('rec');
rec.onresult=e=>{p.value=[...e.results].map(r=>r[0].transcript).join('');p.oninput()};rec.onend=()=>{$('#mic').classList.remove('rec');rec=null};rec.start()};
async function up(space,blob,name){const fd=new FormData();fd.append('files',blob,name);const r=await fetch(space+'/gradio_api/upload',{method:'POST',body:fd});const j=await r.json();return j[0]}
async function run(space,fn,data,save,tries=2){let e='';for(let i=0;i<tries;i++){try{const r=await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'run',space,fn,data,save})});const j=await r.json();if(j.ok&&(!save||String(j.saved).startsWith('http')))return j;e=j.error||j.saved||'failed'}catch(x){e=String(x)}}throw new Error(e)}
async function long(space,fn,data,save){
const s=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'start',space,fn,data})})).json();
if(!s.event_id)throw new Error('start');
const r=await fetch(space+'/gradio_api/call/'+fn+'/'+s.event_id);const rd=r.body.getReader(),dec=new TextDecoder();let t='';
while(true){const {value,done}=await rd.read();if(done)break;t+=dec.decode(value,{stream:true});if(t.length>60000)t=t.slice(-30000);const a=Math.max(t.lastIndexOf('event: complete'),t.lastIndexOf('event: error'));if(a>=0&&t.indexOf('\n\n',a)>0)break}
const m=t.match(/event: complete\s*\ndata: (.*)/);if(!m)throw new Error('render');
const sv=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'save',space,out:JSON.parse(m[1]),save})})).json();
if(!sv.ok)throw new Error('save');return sv.saved;
}
function lineOf(t){const m=t.match(/["“]([^"”]{2,240})["”]/);return m?m[1].trim():''}
function soundOf(t){return 'Natural cinematic sound effects and ambience that match this scene: '+t.replace(/["“][^"”]*["”]/g,'').slice(0,300)}
function card(){const e=feed.querySelector('.empty');if(e)e.remove();const c=document.createElement('div');c.className='card';c.innerHTML='<div class="busy"><div class="ring"></div><span>Wheelie is on it…</span></div>';feed.prepend(c);return c}
async function make(text,opt){
opt=opt||{};const c=card(),msg=c.querySelector('span'),id=Date.now().toString(36)+Math.random().toString(36).slice(2,6);
const say=m=>{msg.textContent=m};
try{
const scene=text.replace(/["“][^"”]*["”]/g,' ').replace(/\s+/g,' ').trim()||'she looks straight into the camera';
const line=lineOf(text);
let start;
if(opt.frame){say('Picking up where she left off…');const pth=await up(WAN,opt.frame,'frame.png');start=FD(pth)}
else{
say('Drawing the first frame…');
const h=wide?720:1280,w=wide?1280:720;
let still;
if(img||useHer){
let refs=[{image:FD(BODY,BODY),caption:null},{image:FD(FACE,FACE),caption:null}];
if(img){const pth=await up(QWEN,img,'ref.png');refs=[{image:FD(pth),caption:null}]}
const pr=(useHer&&!img?LOOK+' ':'Keep the exact people, faces and art style from the reference image. ')+'New scene: '+scene+'. '+(wide?'Horizontal 16:9.':'Vertical 9:16.')+' '+LOOKS;
still=(await run(QWEN,'infer',[refs,pr,0,true,4.0,30,h,w,false],'gen/'+id+'-still.webp')).saved;
}else{
still=(await run(FLUX,'infer',[(scene+'. '+LOOKS).slice(0,1500),0,true,w,h,4],'gen/'+id+'-still.webp')).saved;
}
c.innerHTML='<img src="'+still+'" alt=""><div class="busy" style="background:rgba(1,1,1,.45)"><div class="ring"></div><span>Bringing it to life…</span></div>';
start=FD(still,still);
}
const m2=c.querySelector('span')||msg;const say2=t=>{m2.textContent=t};
let out;
if(line){
const TALK='https://victor-longcat-video-avatar-1-5.hf.space';
say2('Recording her line…');
const v=await (await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'speaktest',voice:'wheelie',text:line,save:id})})).json();
if(!v.ok)throw new Error('voice');
const wav=PUB+'gen/ari/'+id+'.wav';
say2('She\'s saying it in her voice… (a few minutes)');
const tp='A stylized 3D animated young woman in a power wheelchair talks to the camera, lips moving clearly with every word. Her face stays deadpan: flat, unimpressed, slightly amused, no big smile, no big eyebrow raise. Her body is relaxed and moves naturally, like a normal person. She sits normally in her chair. '+scene+'.'+(mode==='custom'?'':MODES[mode])+' Same face, hair, outfit and animation style as the image in every frame.';
out=await long(TALK,'generate',[start,FD(wav,wav),tp.slice(0,1200),'720p',42,'Clean speech (fast)','DBCache faster'],'gen/'+id+'-talk.mp4');
}else{
say2('Bringing it to life…');
const motion=scene+'.'+(mode==='custom'?'':MODES[mode])+' Stylized 3D animated film, same face and style every frame.';
const vid=(await run(WAN,'generate_video',[start,motion.slice(0,1200),6,VNEG,5,1,1,42,true],'gen/'+id+'.mp4')).saved;
say2('Adding the sound…');
out=vid;
try{out=(await run(MMA,'video_to_audio',[FD(vid,vid),soundOf(text),'music, speech, talking, distortion',-1,25,4.5,5],'gen/'+id+'-s.mp4')).saved}catch(_){}
}
c.innerHTML='<video src="'+out+'" crossorigin="anonymous" playsinline muted loop autoplay></video>';
c.onclick=()=>open(out,text);
}catch(_){c.innerHTML='<div class="busy">That one didn\'t come through. Tap to run it again.</div>';c.onclick=()=>{c.remove();make(text,opt)}}
}
function open(url,text){last=url;lastText=text||'';const f=$('#full'),v=f.querySelector('video');v.crossOrigin='anonymous';v.src=url;v.muted=false;f.style.display='flex';v.play()}
$('#full .x').onclick=()=>{const f=$('#full');f.querySelector('video').pause();f.style.display='none'};
$('#sv').onclick=()=>{window.open(last+'?download=wheelie.mp4','_blank')};
$('#ext').onclick=async()=>{const v=$('#full video');v.pause();
try{if(v.duration)v.currentTime=Math.max(0,v.duration-0.05);await new Promise(r=>setTimeout(r,350));const cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;cv.getContext('2d').drawImage(v,0,0);const b=await new Promise(r=>cv.toBlob(r,'image/png'));$('#full .x').onclick();make(p.value.trim()||lastText||'She keeps going, same scene, natural continuation',{frame:b});p.value='';p.oninput()}catch(_){$('#full .x').onclick()}};
$('#go').onclick=()=>{const t=p.value.trim();if(!t&&!img)return;make(t||'She comes alive with cinematic motion');p.value='';p.oninput()};
</script><script type="text/plain">
// Wheelie Create engine v4: Qwen still from her full picture -> talking (LTX-2.5, mouth + voice together) or Wan 2.2 motion + MMAudio.
const SB='https://yzzxvjrzniuhxebqqrsy.supabase.co',GEN=SB+'/functions/v1/gen-call',API=SB+'/functions/v1/wheelie-api',PUB=SB+'/storage/v1/object/public/public/';
const QWEN='https://qwen-qwen-image-edit-2511.hf.space',WAN='https://zerogpu-aoti-wan2-2-fp8da-aoti-faster.hf.space',MMA='https://hkchengrex-mmaudio.hf.space',MIX='https://wayed11-wheelie-mix.hf.space',FLUX='https://wayed11-wheelie-image.hf.space';
const REF='https://wayed11-wheelie-app.static.hf.space/wheelie-full.webp',S2V='https://wan-ai-wan2-2-s2v.hf.space',LTX='https://lightricks-ltx-2-5.hf.space';
const LOOK='Keep this exact woman from the reference image: same adult face, freckles, small beauty mark under one eye, thick low dark brows, heavy-lidded brown eyes, dark brown curly messy high bun, silver hoops, deadpan. Same rainbow chunky knit sweater fully covering her waist, dark jeans, purple high-tops. Same black Quantum Q6 Edge power wheelchair with rainbow LEDs, joystick on the right, she drives it herself.';
const LOOKS='High-end cinematic 3D render, lifelike skin texture, real cinematic lighting, depth of field, film-grade detail. Not cartoonish, not anime, no text, no watermark.';
const VNEG='cartoonish, kids cartoon, chibi, anime, plastic skin, watermark, logo, captions, text, teenager, child, crop top, bare midriff, crying, static frame, blurry, jitter, morphing face, extra fingers, a person pushing the wheelchair';
const MODES={normal:' Smooth natural motion.',fun:' Playful, bouncy, funny comic timing, energetic camera.',cine:' Epic slow cinematic camera move, dramatic lighting.',custom:''};
let mode='normal',img=null,useHer=true,wide=false,last=null,lastText='';
const $=s=>document.querySelector(s),feed=$('#feed');
const FD=(p,u)=>({path:p,url:u||(p.startsWith('http')?p:undefined),meta:{_type:'gradio.FileData'}});
document.querySelector('.empty').innerHTML='<b>Make something.</b>Type or talk your idea. Wheelie stars by default. Put her line in quotes and she says it. Every video comes with sound.';
function showThumb(){const t=$('#thumb');if(img){$('#thumb img').src=URL.createObjectURL(img);$('#thumb span').textContent='Starting from your picture';t.style.display='flex'}else if(useHer){$('#thumb img').src=REF;$('#thumb span').textContent='Starring Wheelie';t.style.display='flex'}else t.style.display='none'}
showThumb();
document.querySelectorAll('.modes button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.modes button').forEach(x=>x.classList.remove('on'));b.classList.add('on');mode=b.dataset.m});
$('#tW').onclick=()=>{wide=!wide;$('#tW').textContent=wide?'16:9':'9:16'};
$('#pic').onclick=()=>$('#file').click();
$('#file').onchange=e=>{const f=e.target.files[0];if(!f)return;img=f;showThumb()};
$('#rmImg').onclick=()=>{if(img){img=null;$('#file').value=''}else useHer=false;showThumb()};
const p=$('#p');p.oninput=()=>{p.style.height='auto';p.style.height=p.scrollHeight+'px'};
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;let rec=null;
$('#mic').onclick=()=>{if(!SR){p.focus();return}if(rec){rec.stop();return}rec=new SR();rec.lang='en-US';rec.interimResults=true;$('#mic').classList.add('rec');
rec.onresult=e=>{p.value=[...e.results].map(r=>r[0].transcript).join('');p.oninput()};rec.onend=()=>{$('#mic').classList.remove('rec');rec=null};rec.start()};
async function up(space,blob,name){const fd=new FormData();fd.append('files',blob,name);const r=await fetch(space+'/gradio_api/upload',{method:'POST',body:fd});const j=await r.json();return j[0]}
async function run(space,fn,data,save,tries=2){let e='';for(let i=0;i<tries;i++){try{const r=await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'run',space,fn,data,save})});const j=await r.json();if(j.ok&&(!save||String(j.saved).startsWith('http')))return j;e=j.error||j.saved||'failed'}catch(x){e=String(x)}}throw new Error(e)}
async function long(space,fn,data,save){
const s=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'start',space,fn,data})})).json();
if(!s.event_id)throw new Error('start');
const r=await fetch(space+'/gradio_api/call/'+fn+'/'+s.event_id);const rd=r.body.getReader(),dec=new TextDecoder();let t='';
while(true){const {value,done}=await rd.read();if(done)break;t+=dec.decode(value,{stream:true});if(t.length>60000)t=t.slice(-30000);const a=Math.max(t.lastIndexOf('event: complete'),t.lastIndexOf('event: error'));if(a>=0&&t.indexOf('\n\n',a)>0)break}
const m=t.match(/event: complete\s*\ndata: (.*)/);if(!m)throw new Error('render');
const sv=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'save',space,out:JSON.parse(m[1]),save})})).json();
if(!sv.ok)throw new Error('save');return sv.saved;
}
function lineOf(t){const m=t.match(/["“]([^"”]{2,240})["”]/);return m?m[1].trim():''}
function soundOf(t){return 'Natural cinematic sound effects and ambience that match this scene: '+t.replace(/["“][^"”]*["”]/g,'').slice(0,300)}
function card(){const e=feed.querySelector('.empty');if(e)e.remove();const c=document.createElement('div');c.className='card';c.innerHTML='<div class="busy"><div class="ring"></div><span>Wheelie is on it…</span></div>';feed.prepend(c);return c}
async function make(text,opt){
opt=opt||{};const c=card(),msg=c.querySelector('span'),id=Date.now().toString(36)+Math.random().toString(36).slice(2,6);
const say=m=>{msg.textContent=m};
try{
const scene=text.replace(/["“][^"”]*["”]/g,' ').replace(/\s+/g,' ').trim()||'she looks straight into the camera';
const line=lineOf(text);
let start;
if(opt.frame){say('Picking up where she left off…');const pth=await up(WAN,opt.frame,'frame.png');start=FD(pth)}
else{
say('Drawing the first frame…');
const h=wide?720:1280,w=wide?1280:720;
let still;
if(img||useHer){
let ref=FD(REF,REF);if(img){const pth=await up(QWEN,img,'ref.png');ref=FD(pth)}
const pr=(useHer&&!img?LOOK+' ':'Keep the exact people, faces and style from the reference image. ')+'NEW SCENE: '+scene+'. '+(wide?'Horizontal 16:9. ':'Vertical 9:16. ')+LOOKS;
still=(await run(QWEN,'infer',[[{image:ref,caption:null}],pr,0,true,4.0,20,h,w,false],'gen/'+id+'-still.webp')).saved;
}else{
still=(await run(FLUX,'infer',[(scene+'. '+LOOKS).slice(0,1500),0,true,w,h,4],'gen/'+id+'-still.webp')).saved;
}
c.innerHTML='<img src="'+still+'" alt=""><div class="busy" style="background:rgba(1,1,1,.45)"><div class="ring"></div><span>Bringing it to life…</span></div>';
start=FD(still,still);
}
const m2=c.querySelector('span')||msg;const say2=t=>{m2.textContent=t};
let out;
if(line){
say2('She\'s saying it…');
const words=line.split(/\s+/).length,longLine=words>12;
const tp=scene+'. She looks into the camera and talks, her lips clearly moving with every word. She says in a warm, dry New York accent, young woman\'s voice: "'+line+'"'+(mode==='custom'?'':MODES[mode])+' Natural sound of the scene behind her. Her face, hair, outfit, wheelchair and art style stay exactly the same as the image in every frame.';
const hh=wide?832:1472,ww=wide?1472:832;
out=(await run(LTX,'run',[tp.slice(0,1500),start,hh,ww,Math.min(5,Math.max(3,words/2.4+1.2)),42,'conv',longLine,true,false],'gen/'+id+'-talk.mp4')).saved;
}else{
say2('Bringing it to life…');
const motion=scene+'.'+(mode==='custom'?'':MODES[mode])+' Cinematic, same face every frame.';
const vid=(await run(WAN,'generate_video',[start,motion.slice(0,1200),6,VNEG,5,1,1,42,true],'gen/'+id+'.mp4')).saved;
say2('Adding the sound…');
out=vid;
try{out=(await run(MMA,'video_to_audio',[FD(vid,vid),soundOf(text),'music, speech, talking, distortion',-1,25,4.5,5],'gen/'+id+'-s.mp4')).saved}catch(_){}
}
c.innerHTML='<video src="'+out+'" crossorigin="anonymous" playsinline muted loop autoplay></video>';
c.onclick=()=>open(out,text);
}catch(_){c.innerHTML='<div class="busy">That one didn\'t come through. Tap to run it again.</div>';c.onclick=()=>{c.remove();make(text,opt)}}
}
function open(url,text){last=url;lastText=text||'';const f=$('#full'),v=f.querySelector('video');v.crossOrigin='anonymous';v.src=url;v.muted=false;f.style.display='flex';v.play()}
$('#full .x').onclick=()=>{const f=$('#full');f.querySelector('video').pause();f.style.display='none'};
$('#sv').onclick=()=>{window.open(last+'?download=wheelie.mp4','_blank')};
$('#ext').onclick=async()=>{const v=$('#full video');v.pause();
try{if(v.duration)v.currentTime=Math.max(0,v.duration-0.05);await new Promise(r=>setTimeout(r,350));const cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;cv.getContext('2d').drawImage(v,0,0);const b=await new Promise(r=>cv.toBlob(r,'image/png'));$('#full .x').onclick();make(p.value.trim()||lastText||'She keeps going, same scene, natural continuation',{frame:b});p.value='';p.oninput()}catch(_){$('#full .x').onclick()}};
$('#go').onclick=()=>{const t=p.value.trim();if(!t&&!img)return;make(t||'She comes alive with cinematic motion');p.value='';p.oninput()};
</script><script type="text/plain">
// Wheelie Create engine v3: Qwen still from her full picture -> talking (Wan S2V + her voice) or Wan 2.2 motion -> MMAudio scene sound.
const SB='https://yzzxvjrzniuhxebqqrsy.supabase.co',GEN=SB+'/functions/v1/gen-call',API=SB+'/functions/v1/wheelie-api',PUB=SB+'/storage/v1/object/public/public/';
const QWEN='https://qwen-qwen-image-edit-2511.hf.space',WAN='https://zerogpu-aoti-wan2-2-fp8da-aoti-faster.hf.space',MMA='https://hkchengrex-mmaudio.hf.space',MIX='https://wayed11-wheelie-mix.hf.space',FLUX='https://wayed11-wheelie-image.hf.space';
const REF='https://wayed11-wheelie-app.static.hf.space/wheelie-full.webp',S2V='https://wan-ai-wan2-2-s2v.hf.space';
const LOOK='Keep this exact woman from the reference image: same adult face, freckles, small beauty mark under one eye, thick low dark brows, heavy-lidded brown eyes, dark brown curly messy high bun, silver hoops, deadpan. Same rainbow chunky knit sweater fully covering her waist, dark jeans, purple high-tops. Same black Quantum Q6 Edge power wheelchair with rainbow LEDs, joystick on the right, she drives it herself.';
const LOOKS='High-end cinematic 3D render, lifelike skin texture, real cinematic lighting, depth of field, film-grade detail. Not cartoonish, not anime, no text, no watermark.';
const VNEG='cartoonish, kids cartoon, chibi, anime, plastic skin, watermark, logo, captions, text, teenager, child, crop top, bare midriff, crying, static frame, blurry, jitter, morphing face, extra fingers, a person pushing the wheelchair';
const MODES={normal:' Smooth natural motion.',fun:' Playful, bouncy, funny comic timing, energetic camera.',cine:' Epic slow cinematic camera move, dramatic lighting.',custom:''};
let mode='normal',img=null,useHer=true,wide=false,last=null,lastText='';
const $=s=>document.querySelector(s),feed=$('#feed');
const FD=(p,u)=>({path:p,url:u||(p.startsWith('http')?p:undefined),meta:{_type:'gradio.FileData'}});
document.querySelector('.empty').innerHTML='<b>Make something.</b>Type or talk your idea. Wheelie stars by default. Put her line in quotes and she says it. Every video comes with sound.';
function showThumb(){const t=$('#thumb');if(img){$('#thumb img').src=URL.createObjectURL(img);$('#thumb span').textContent='Starting from your picture';t.style.display='flex'}else if(useHer){$('#thumb img').src=REF;$('#thumb span').textContent='Starring Wheelie';t.style.display='flex'}else t.style.display='none'}
showThumb();
document.querySelectorAll('.modes button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.modes button').forEach(x=>x.classList.remove('on'));b.classList.add('on');mode=b.dataset.m});
$('#tW').onclick=()=>{wide=!wide;$('#tW').textContent=wide?'16:9':'9:16'};
$('#pic').onclick=()=>$('#file').click();
$('#file').onchange=e=>{const f=e.target.files[0];if(!f)return;img=f;showThumb()};
$('#rmImg').onclick=()=>{if(img){img=null;$('#file').value=''}else useHer=false;showThumb()};
const p=$('#p');p.oninput=()=>{p.style.height='auto';p.style.height=p.scrollHeight+'px'};
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;let rec=null;
$('#mic').onclick=()=>{if(!SR){p.focus();return}if(rec){rec.stop();return}rec=new SR();rec.lang='en-US';rec.interimResults=true;$('#mic').classList.add('rec');
rec.onresult=e=>{p.value=[...e.results].map(r=>r[0].transcript).join('');p.oninput()};rec.onend=()=>{$('#mic').classList.remove('rec');rec=null};rec.start()};
async function up(space,blob,name){const fd=new FormData();fd.append('files',blob,name);const r=await fetch(space+'/gradio_api/upload',{method:'POST',body:fd});const j=await r.json();return j[0]}
async function run(space,fn,data,save,tries=2){let e='';for(let i=0;i<tries;i++){try{const r=await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'run',space,fn,data,save})});const j=await r.json();if(j.ok&&(!save||String(j.saved).startsWith('http')))return j;e=j.error||j.saved||'failed'}catch(x){e=String(x)}}throw new Error(e)}
async function long(space,fn,data,save){
const s=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'start',space,fn,data})})).json();
if(!s.event_id)throw new Error('start');
const r=await fetch(space+'/gradio_api/call/'+fn+'/'+s.event_id);const rd=r.body.getReader(),dec=new TextDecoder();let t='';
while(true){const {value,done}=await rd.read();if(done)break;t+=dec.decode(value,{stream:true});if(t.length>60000)t=t.slice(-30000);const a=Math.max(t.lastIndexOf('event: complete'),t.lastIndexOf('event: error'));if(a>=0&&t.indexOf('\n\n',a)>0)break}
const m=t.match(/event: complete\s*\ndata: (.*)/);if(!m)throw new Error('render');
const sv=await (await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'save',space,out:JSON.parse(m[1]),save})})).json();
if(!sv.ok)throw new Error('save');return sv.saved;
}
function lineOf(t){const m=t.match(/["“]([^"”]{2,240})["”]/);return m?m[1].trim():''}
function soundOf(t){return 'Natural cinematic sound effects and ambience that match this scene: '+t.replace(/["“][^"”]*["”]/g,'').slice(0,300)}
function card(){const e=feed.querySelector('.empty');if(e)e.remove();const c=document.createElement('div');c.className='card';c.innerHTML='<div class="busy"><div class="ring"></div><span>Wheelie is on it…</span></div>';feed.prepend(c);return c}
async function make(text,opt){
opt=opt||{};const c=card(),msg=c.querySelector('span'),id=Date.now().toString(36)+Math.random().toString(36).slice(2,6);
const say=m=>{msg.textContent=m};
try{
const scene=text.replace(/["“][^"”]*["”]/g,' ').replace(/\s+/g,' ').trim()||'she looks straight into the camera';
const line=lineOf(text);
let start;
if(opt.frame){say('Picking up where she left off…');const pth=await up(WAN,opt.frame,'frame.png');start=FD(pth)}
else{
say('Drawing the first frame…');
const h=wide?720:1280,w=wide?1280:720;
let still;
if(img||useHer){
let ref=FD(REF,REF);if(img){const pth=await up(QWEN,img,'ref.png');ref=FD(pth)}
const pr=(useHer&&!img?LOOK+' ':'Keep the exact people, faces and style from the reference image. ')+'NEW SCENE: '+scene+'. '+(wide?'Horizontal 16:9. ':'Vertical 9:16. ')+LOOKS;
still=(await run(QWEN,'infer',[[{image:ref,caption:null}],pr,0,true,4.0,20,h,w,false],'gen/'+id+'-still.webp')).saved;
}else{
still=(await run(FLUX,'infer',[(scene+'. '+LOOKS).slice(0,1500),0,true,w,h,4],'gen/'+id+'-still.webp')).saved;
}
c.innerHTML='<img src="'+still+'" alt=""><div class="busy" style="background:rgba(1,1,1,.45)"><div class="ring"></div><span>Bringing it to life…</span></div>';
start=FD(still,still);
}
const m2=c.querySelector('span')||msg;const say2=t=>{m2.textContent=t};
let out;
if(line){
say2('Recording her line…');
const v=await (await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'speaktest',voice:'wheelie',text:line,save:id})})).json();
if(!v.ok)throw new Error('voice');
const wav=PUB+'gen/ari/'+id+'.wav';
say2('She\'s saying it… (about 4 min)');
const talk=await long(S2V,'predict',[start,FD(wav,wav),'480P'],'gen/'+id+'-talk.mp4');
out=talk;
say2('Adding the scene sound…');
try{const amb=(await run(MMA,'video_to_audio',[FD(talk,talk),soundOf(text),'music, speech, talking, voice, distortion',-1,25,4.5,8],'gen/'+id+'-s.mp4')).saved;
out=(await run(MIX,'mix',[amb,wav,0,1.6],'gen/'+id+'-final.mp4')).saved}catch(_){}
}else{
say2('Bringing it to life…');
const motion=scene+'.'+(mode==='custom'?'':MODES[mode])+' Cinematic, same face every frame.';
const vid=(await run(WAN,'generate_video',[start,motion.slice(0,1200),6,VNEG,5,1,1,42,true],'gen/'+id+'.mp4')).saved;
say2('Adding the sound…');
out=vid;
try{out=(await run(MMA,'video_to_audio',[FD(vid,vid),soundOf(text),'music, speech, talking, distortion',-1,25,4.5,5],'gen/'+id+'-s.mp4')).saved}catch(_){}
}
c.innerHTML='<video src="'+out+'" crossorigin="anonymous" playsinline muted loop autoplay></video>';
c.onclick=()=>open(out,text);
}catch(_){c.innerHTML='<div class="busy">That one didn\'t come through. Tap to run it again.</div>';c.onclick=()=>{c.remove();make(text,opt)}}
}
function open(url,text){last=url;lastText=text||'';const f=$('#full'),v=f.querySelector('video');v.crossOrigin='anonymous';v.src=url;v.muted=false;f.style.display='flex';v.play()}
$('#full .x').onclick=()=>{const f=$('#full');f.querySelector('video').pause();f.style.display='none'};
$('#sv').onclick=()=>{window.open(last+'?download=wheelie.mp4','_blank')};
$('#ext').onclick=async()=>{const v=$('#full video');v.pause();
try{if(v.duration)v.currentTime=Math.max(0,v.duration-0.05);await new Promise(r=>setTimeout(r,350));const cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;cv.getContext('2d').drawImage(v,0,0);const b=await new Promise(r=>cv.toBlob(r,'image/png'));$('#full .x').onclick();make(p.value.trim()||lastText||'She keeps going, same scene, natural continuation',{frame:b});p.value='';p.oninput()}catch(_){$('#full .x').onclick()}};
$('#go').onclick=()=>{const t=p.value.trim();if(!t&&!img)return;make(t||'She comes alive with cinematic motion');p.value='';p.oninput()};
</script><script type="text/plain">
// Wheelie Create engine v2: Qwen still (her locked look) -> Wan 2.2 motion -> MMAudio scene sound -> her voice on top.
const SB='https://yzzxvjrzniuhxebqqrsy.supabase.co',GEN=SB+'/functions/v1/gen-call',API=SB+'/functions/v1/wheelie-api',PUB=SB+'/storage/v1/object/public/public/';
const QWEN='https://qwen-qwen-image-edit-2511.hf.space',WAN='https://zerogpu-aoti-wan2-2-fp8da-aoti-faster.hf.space',MMA='https://hkchengrex-mmaudio.hf.space',MIX='https://wayed11-wheelie-mix.hf.space',FLUX='https://wayed11-wheelie-image.hf.space';
const REF='https://wayed11-wheelie-app.static.hf.space/wheelie.jpg';
const LOOK='Keep this exact woman from the reference image: same adult face, freckles, small beauty mark under one eye, thick low dark brows, heavy-lidded brown eyes, dark brown curly messy high bun, silver hoops, deadpan. Same rainbow chunky knit sweater fully covering her waist, dark jeans, purple high-tops. Same black Quantum Q6 Edge power wheelchair with rainbow LEDs, joystick on the right, she drives it herself.';
const LOOKS='High-end cinematic 3D render, lifelike skin texture, real cinematic lighting, depth of field, film-grade detail. Not cartoonish, not anime, no text, no watermark.';
const VNEG='cartoonish, kids cartoon, chibi, anime, plastic skin, watermark, logo, captions, text, teenager, child, crop top, bare midriff, crying, static frame, blurry, jitter, morphing face, extra fingers, a person pushing the wheelchair';
const MODES={normal:' Smooth natural motion.',fun:' Playful, bouncy, funny comic timing, energetic camera.',cine:' Epic slow cinematic camera move, dramatic lighting.',custom:''};
let mode='normal',img=null,useHer=true,wide=false,last=null,lastText='';
const $=s=>document.querySelector(s),feed=$('#feed');
const FD=(p,u)=>({path:p,url:u||(p.startsWith('http')?p:undefined),meta:{_type:'gradio.FileData'}});
document.querySelector('.empty').innerHTML='<b>Make something.</b>Type or talk your idea. Wheelie stars by default. Put her line in quotes and she says it. Every video comes with sound.';
function showThumb(){const t=$('#thumb');if(img){$('#thumb img').src=URL.createObjectURL(img);$('#thumb span').textContent='Starting from your picture';t.style.display='flex'}else if(useHer){$('#thumb img').src=REF;$('#thumb span').textContent='Starring Wheelie';t.style.display='flex'}else t.style.display='none'}
showThumb();
document.querySelectorAll('.modes button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.modes button').forEach(x=>x.classList.remove('on'));b.classList.add('on');mode=b.dataset.m});
$('#tW').onclick=()=>{wide=!wide;$('#tW').textContent=wide?'16:9':'9:16'};
$('#pic').onclick=()=>$('#file').click();
$('#file').onchange=e=>{const f=e.target.files[0];if(!f)return;img=f;showThumb()};
$('#rmImg').onclick=()=>{if(img){img=null;$('#file').value=''}else useHer=false;showThumb()};
const p=$('#p');p.oninput=()=>{p.style.height='auto';p.style.height=p.scrollHeight+'px'};
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;let rec=null;
$('#mic').onclick=()=>{if(!SR){p.focus();return}if(rec){rec.stop();return}rec=new SR();rec.lang='en-US';rec.interimResults=true;$('#mic').classList.add('rec');
rec.onresult=e=>{p.value=[...e.results].map(r=>r[0].transcript).join('');p.oninput()};rec.onend=()=>{$('#mic').classList.remove('rec');rec=null};rec.start()};
async function up(space,blob,name){const fd=new FormData();fd.append('files',blob,name);const r=await fetch(space+'/gradio_api/upload',{method:'POST',body:fd});const j=await r.json();return j[0]}
async function run(space,fn,data,save,tries=2){let e='';for(let i=0;i<tries;i++){try{const r=await fetch(GEN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({op:'run',space,fn,data,save})});const j=await r.json();if(j.ok&&(!save||String(j.saved).startsWith('http')))return j;e=j.error||j.saved||'failed'}catch(x){e=String(x)}}throw new Error(e)}
function lineOf(t){const m=t.match(/["“]([^"”]{2,240})["”]/);return m?m[1].trim():''}
function soundOf(t){return 'Natural cinematic sound effects and ambience that match this scene: '+t.replace(/["“][^"”]*["”]/g,'').slice(0,300)}
function card(){const e=feed.querySelector('.empty');if(e)e.remove();const c=document.createElement('div');c.className='card';c.innerHTML='<div class="busy"><div class="ring"></div><span>Wheelie is on it…</span></div>';feed.prepend(c);return c}
async function make(text,opt){
opt=opt||{};const c=card(),msg=c.querySelector('span'),id=Date.now().toString(36)+Math.random().toString(36).slice(2,6);
const say=m=>{msg.textContent=m};
try{
const scene=text.replace(/["“][^"”]*["”]/g,' ').replace(/\s+/g,' ').trim()||'she looks straight into the camera';
const line=lineOf(text);
let start;
if(opt.frame){say('Picking up where she left off…');const pth=await up(WAN,opt.frame,'frame.png');start=FD(pth)}
else{
say('Drawing the first frame…');
const h=wide?720:1280,w=wide?1280:720;
let still;
if(img||useHer){
let ref=FD(REF,REF);if(img){const pth=await up(QWEN,img,'ref.png');ref=FD(pth)}
const pr=(useHer&&!img?LOOK+' ':'Keep the exact people, faces and style from the reference image. ')+'NEW SCENE: '+scene+'. '+(wide?'Horizontal 16:9. ':'Vertical 9:16. ')+LOOKS;
still=(await run(QWEN,'infer',[[{image:ref,caption:null}],pr,0,true,4.0,20,h,w,false],'gen/'+id+'-still.webp')).saved;
}else{
still=(await run(FLUX,'infer',[(scene+'. '+LOOKS).slice(0,1500),0,true,w,h,4],'gen/'+id+'-still.webp')).saved;
}
c.innerHTML='<img src="'+still+'" alt=""><div class="busy" style="background:rgba(1,1,1,.45)"><div class="ring"></div><span>Bringing it to life…</span></div>';
start=FD(still,still);
}
const m2=c.querySelector('span')||msg;const say2=t=>{m2.textContent=t};
say2('Bringing it to life…');
const motion=scene+'.'+(mode==='custom'?'':MODES[mode])+' Cinematic, same face every frame.';
const vid=(await run(WAN,'generate_video',[start,motion.slice(0,1200),6,VNEG,5,1,1,42,true],'gen/'+id+'.mp4')).saved;
say2('Adding the sound…');
let out=vid;
try{out=(await run(MMA,'video_to_audio',[FD(vid,vid),soundOf(text),'music, speech, talking, distortion',-1,25,4.5,5],'gen/'+id+'-s.mp4')).saved}catch(_){}
if(line){
say2('Recording her line…');
try{
const v=await (await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'speaktest',voice:'wheelie',text:line,save:id})})).json();
if(v.ok)out=(await run(MIX,'mix',[out,PUB+'gen/ari/'+id+'.wav',0.5,1.6],'gen/'+id+'-final.mp4')).saved;
}catch(_){}
}
c.innerHTML='<video src="'+out+'" crossorigin="anonymous" playsinline muted loop autoplay></video>';
c.onclick=()=>open(out,text);
}catch(_){c.innerHTML='<div class="busy">That one didn\'t come through. Tap to run it again.</div>';c.onclick=()=>{c.remove();make(text,opt)}}
}
function open(url,text){last=url;lastText=text||'';const f=$('#full'),v=f.querySelector('video');v.crossOrigin='anonymous';v.src=url;v.muted=false;f.style.display='flex';v.play()}
$('#full .x').onclick=()=>{const f=$('#full');f.querySelector('video').pause();f.style.display='none'};
$('#sv').onclick=()=>{window.open(last+'?download=wheelie.mp4','_blank')};
$('#ext').onclick=async()=>{const v=$('#full video');v.pause();
try{if(v.duration)v.currentTime=Math.max(0,v.duration-0.05);await new Promise(r=>setTimeout(r,350));const cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;cv.getContext('2d').drawImage(v,0,0);const b=await new Promise(r=>cv.toBlob(r,'image/png'));$('#full .x').onclick();make(p.value.trim()||lastText||'She keeps going, same scene, natural continuation',{frame:b});p.value='';p.oninput()}catch(_){$('#full .x').onclick()}};
$('#go').onclick=()=>{const t=p.value.trim();if(!t&&!img)return;make(t||'She comes alive with cinematic motion');p.value='';p.oninput()};
</script><script type="text/plain">
const ENG='https://wayed11-wheelie-ovi.hf.space';
const STYLE=' Vibrant 3D animated cartoon style, cinematic lighting, smooth expressive motion, rich synced sound effects and music.';
const MODES={normal:'',fun:' Playful, bouncy, funny, energetic.',cine:' Epic cinematic camera move, dramatic lighting, film score.',custom:''};
let mode='normal',img=null,wide=false,last=null;
const $=s=>document.querySelector(s),feed=$('#feed');
document.querySelectorAll('.modes button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.modes button').forEach(x=>x.classList.remove('on'));b.classList.add('on');mode=b.dataset.m});
$('#tW').onclick=()=>{wide=!wide;$('#tW').textContent=wide?'16:9':'9:16'};
$('#pic').onclick=()=>$('#file').click();
$('#file').onchange=e=>{const f=e.target.files[0];if(!f)return;img=f;$('#thumb img').src=URL.createObjectURL(f);$('#thumb').style.display='flex'};
$('#rmImg').onclick=()=>{img=null;$('#thumb').style.display='none';$('#file').value=''};
const p=$('#p');p.oninput=()=>{p.style.height='auto';p.style.height=p.scrollHeight+'px'};
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;let rec=null;
$('#mic').onclick=()=>{if(!SR){p.focus();return}if(rec){rec.stop();return}rec=new SR();rec.lang='en-US';rec.interimResults=true;$('#mic').classList.add('rec');
rec.onresult=e=>{p.value=[...e.results].map(r=>r[0].transcript).join('');p.oninput()};rec.onend=()=>{$('#mic').classList.remove('rec');rec=null};rec.start()};
async function upload(blob,name){const fd=new FormData();fd.append('files',blob,name||'frame.png');const r=await fetch(ENG+'/gradio_api/upload',{method:'POST',body:fd});const j=await r.json();return {path:j[0],meta:{_type:'gradio.FileData'},orig_name:name||'frame.png'}}
function card(){const e=feed.querySelector('.empty');if(e)e.remove();const c=document.createElement('div');c.className='card';c.innerHTML='<div class="busy"><div class="ring"></div><span>Wheelie is making it…</span></div>';feed.prepend(c);return c}
async function make(text,startBlob){
const c=card(),msg=c.querySelector('span');
try{
const ff=startBlob?await upload(startBlob,'start.png'):null;
const prompt=text+(mode==='custom'?'':MODES[mode]+STYLE);
const h=wide?512:768,w=wide?768:512;
const r=await fetch(ENG+'/gradio_api/call/generate_scene',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:[prompt+' <AUDCAP>Clear synced voices, sound effects and music that match the scene.<ENDAUDCAP>',30,ff]})});
const {event_id}=await r.json();if(!event_id)throw 0;
msg.textContent='Rendering video and sound…';
const s=await fetch(ENG+'/gradio_api/call/generate_scene/'+event_id);const t=await s.text();
const m=t.match(/event: complete\s*\ndata: (.*)/);if(!m)throw 0;
const d=JSON.parse(m[1]);const v=(Array.isArray(d)?d[0]:d);const url=(v&&(v.url||(v.video&&v.video.url)))||'';
if(!url)throw 0;
c.innerHTML='<video src="'+url+'" playsinline muted loop autoplay></video>';
c.onclick=()=>open(url);
}catch(_){c.innerHTML='<div class="busy">That one didn\'t come through. Tap to try again.</div>';c.onclick=()=>{c.remove();make(text,startBlob)}}
}
function open(url){last=url;const f=$('#full'),v=f.querySelector('video');v.src=url;v.muted=false;f.style.display='flex';v.play()}
$('#full .x').onclick=()=>{const f=$('#full');f.querySelector('video').pause();f.style.display='none'};
$('#sv').onclick=()=>{const a=document.createElement('a');a.href=last;a.download='wheelie.mp4';a.click()};
$('#ext').onclick=async()=>{const v=$('#full video');v.pause();const cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;
try{cv.getContext('2d').drawImage(v,0,0);const b=await new Promise(r=>cv.toBlob(r,'image/png'));$('#full .x').onclick();make(p.value.trim()||'Continue the scene naturally',b)}catch(_){$('#full .x').onclick()}};
$('#go').onclick=()=>{const t=p.value.trim();if(!t&&!img)return;make(t||'Make this image come alive with cinematic motion',img);p.value='';p.oninput()};
</script></body></html>