Spaces:
Running
Running
File size: 45,345 Bytes
939d803 66a9542 1f4049b f5032e0 1f4049b 66a9542 826bc25 1dd2bb0 2324b0a 024d8a8 939d803 024d8a8 939d803 024d8a8 939d803 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | <!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> |