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>