-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.js
More file actions
1273 lines (1207 loc) · 83.1 KB
/
Copy pathbot.js
File metadata and controls
1273 lines (1207 loc) · 83.1 KB
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
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// maarmapa — Telegram Bot v7
// Claude + Grok + Runway + Seedance + DeepSeek + Shotstack + R2
// v7.5 — comandos de modelos vía OpenRouter (seedance mini/4k, seedream, omni flash, seed audio) + /probemodelos
const AGENT_URL = process.env.AGENT_URL || 'https://maarmapa-agent.onrender.com';
const TELEGRAM_TOKEN = process.env.TELEGRAM_TOKEN;
const OPENROUTER_KEY = process.env.OPENROUTER_KEY || process.env.OPENROUT_KEY;
const TokenMonitor = require('./token-monitor');
const runs = require('./run-store');
const WebPostGenerator = require('./webpost-module');
const WebPostCarouselGenerator = require('./webpost-carousel-module');
const WebPostHaikuImages = require('./webpost-haiku-images-ultra-simple');
const WebPostHyperframes = require('./webpost-hyperframes-module');
const WebPostHaikuAdobeMCP = require('./webpost-haiku-adobe-mcp-module');
const WebPostOpenRouter = require('./webpost-openrouter-module');
const SOUTHSIDE_AUDIO = 'https://pub-5dd65bdf9977446c93204c83d30ec735.r2.dev/SOUTH%20SIDE%20CRIMINI.mp3';
const R2_BASE = 'https://pub-5dd65bdf9977446c93204c83d30ec735.r2.dev/';
const R2_WORKER = 'https://maarmapa-media.mario-25d.workers.dev';
// Token de subida al worker de R2. Antes el PUT del worker no pedia nada:
// cualquiera con la URL podia subir archivos al bucket o pisar los que ya
// estaban. Va por entorno, nunca en el codigo.
const R2_UPLOAD_TOKEN = process.env.R2_UPLOAD_TOKEN || '';
const MODELS = {
text: { fast: 'deepseek/deepseek-chat', pro: 'deepseek/deepseek-r1', gpt: 'openai/gpt-4o' },
video: { seedance_fast: 'bytedance/seedance-2.0-fast', seedance: 'bytedance/seedance-2.0', veo: 'google/veo-3.1' }
};
// v7.5 — los comandos nuevos resuelven el ID real contra GET /models de OpenRouter (ver resolveOR)
let currentTextModel = MODELS.text.fast;
let currentVideoModel = MODELS.video.seedance_fast;
const monitor = new TokenMonitor();
monitor.checkBalances();
setInterval(() => monitor.checkBalances(), 600000);
const webPostGen = new WebPostGenerator(OPENROUTER_KEY, TELEGRAM_TOKEN, R2_WORKER);
const webpostHaikuImages = new WebPostHaikuImages();
const webpostHyperframes = new WebPostHyperframes();
const webpostHaikuAdobe = new WebPostHaikuAdobeMCP();
const webpostOpenRouter = new WebPostOpenRouter();
const carouselGen = new WebPostCarouselGenerator({
openrouterKey: OPENROUTER_KEY,
anthropicKey: process.env.ANTHROPIC_KEY,
grokKey: process.env.GROK_KEY,
runwayKey: process.env.RUNWAY_KEY,
telegramToken: TELEGRAM_TOKEN,
r2Worker: R2_WORKER,
searchProvider: 'duckduckgo',
imageGenerator: 'webimages'
});
const clipStore = {};
function saveClip(chatId, url) {
if (!clipStore[chatId]) clipStore[chatId] = [];
if (url && !clipStore[chatId].includes(url)) {
clipStore[chatId].push(url);
if (clipStore[chatId].length > 30) clipStore[chatId].shift();
}
}
function getClips(chatId) { return clipStore[chatId] || []; }
function clearClips(chatId) { clipStore[chatId] = []; }
// Character bibles
const BASE_STYLE = 'Hyper-cinematic dark anime. Katsuhiro Otomo Akira meets Wu-Tang Clan 36 Chambers. Cel-shading heavy brush ink textures. Mature ultra-dark occult. NOT kawaii NOT chibi. Black ink shadows. Black incense smoke. Film grain heavy. Black magic ritual energy. Blood moon. Only deep blacks blood crimsons tarnished gold shadow teal.';
const CITY_BG = 'Background: abandoned Shaolin monastery ruins in dark Santiago barrio night. Crumbling stone arches with faded Chinese ink paintings and Spanish graffiti tags. Thick black smoke at ground level. Single blood-red lantern far away. Andes mountain silhouette through storm clouds. Wet black cobblestones with ancient symbols. Only candlelight and blood-red moon through storm clouds.';
const ANDINO_P = 'ANDINO full PITCH MATTE BLACK ninja suit ONLY two calm predator eyes glowing faint crimson. Battle-scarred crimson red headphones over ninja hood. Ancient MPC drum machine at feet like ritual altar red runes glowing dark smoke rising. Monk-still posture. BACKGROUND center partially in shadow. Dark smoke curls around him.';
const PIERO_P = 'PIERO full CHARCOAL BLACK ninja suit aged worn texture ONLY sharp eyes behind thick-frame scarred glasses over mask. Holding black iron microphone upward like ritual weapon free hand Shaolin open-palm strike. FOREGROUND LEFT. Tarnished gold clan seal on chest. Shadow across half his face.';
const KINNY_P = 'KINNY full DEEP SHADOW ninja suit dark teal energy pulsing in markings like veins. ONLY fierce feral predator eyes. FOREGROUND RIGHT body coiled low explosive Shaolin stance knees bent weight forward one hand on wet ground other arm cocked back. Three obsidian shuriken on belt. Dark smoke at feet.';
const SQUAD_COMP = 'COMPOSITION: PIERO foreground left, KINNY foreground right, ANDINO center-background partially in shadow smaller. Three-point triangle depth. All fully visible inside safe margins. Blood red moon above. Heavy black smoke atmosphere.';
// Telegram helpers
async function tg(method, body) {
const res = await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/' + method, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
signal: AbortSignal.timeout(60000)
});
return (await res.json()).result;
}
async function send(chatId, text) { return (await tg('sendMessage', { chat_id: chatId, text, parse_mode: 'Markdown' }))?.message_id; }
async function edit(chatId, msgId, text) { try { await tg('editMessageText', { chat_id: chatId, message_id: msgId, text, parse_mode: 'Markdown' }); } catch(e) {} }
async function photo(chatId, url, caption) {
try {
const r = await tg('sendPhoto', { chat_id: chatId, photo: url, caption });
if (r) return;
const vr = await fetch(url, { signal: AbortSignal.timeout(15000) });
if (!vr.ok) return;
const vb = await vr.arrayBuffer();
const form = new FormData();
form.append('chat_id', String(chatId));
form.append('photo', new Blob([vb], { type: 'image/jpeg' }), 'image.jpg');
if (caption) form.append('caption', caption);
await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/sendPhoto', { method: 'POST', body: form, signal: AbortSignal.timeout(20000) });
} catch(e) { console.error('Photo error:', e.message); }
}
async function video(chatId, url, caption) {
try { await tg('sendVideo', { chat_id: chatId, video: url, caption }); saveClip(chatId, url); } catch(e) { console.error('sendVideo failed:', e.message); }
}
function bar(n, t) { const f = Math.round((n/t)*10); return '[' + '█'.repeat(f) + '░'.repeat(10-f) + '] ' + Math.round((n/t)*100) + '%'; }
// R2 upload via Worker
async function uploadToR2(buffer, filename, contentType) {
try {
const r = await fetch(R2_WORKER + '/' + filename, {
method: 'PUT',
headers: {
'Content-Type': contentType || 'video/mp4',
'Authorization': 'Bearer ' + R2_UPLOAD_TOKEN
},
body: buffer
});
const d = await r.json();
console.log('R2 upload:', d.url);
return d.url || null;
} catch(e) { console.error('R2 error:', e.message); return null; }
}
// Grok image
async function grokImg(prompt) {
if (!process.env.GROK_KEY) return null;
try {
const r = await fetch('https://api.x.ai/v1/images/generations', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.GROK_KEY },
body: JSON.stringify({ model: 'grok-imagine-image', prompt, n: 1, response_format: 'url' })
});
const grokUrl = (await r.json()).data?.[0]?.url || null;
if (!grokUrl) return null;
try {
const imgRes = await fetch(grokUrl);
const imgBuf = await imgRes.arrayBuffer();
const r2Url = await uploadToR2(imgBuf, 'grok_' + Date.now() + '.jpg', 'image/jpeg');
return r2Url || grokUrl;
} catch(e) { return grokUrl; }
} catch(e) { return null; }
}
// Runway
async function runwayVideo(imageUrl, prompt, duration) {
if (!process.env.RUNWAY_KEY) return null;
try {
const r = await fetch('https://api.dev.runwayml.com/v1/image_to_video', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({ model: 'gen4_turbo', promptImage: imageUrl, promptText: prompt, ratio: '720:1280', duration: duration || 5 })
});
const txt = await r.text();
let d; try { d = JSON.parse(txt); } catch(e) { return null; }
if (!d.id) return null;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const t = await (await fetch('https://api.dev.runwayml.com/v1/tasks/' + d.id, { headers: { 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' } })).json();
if (t.status === 'SUCCEEDED') return t.output?.[0] || null;
if (t.status === 'FAILED') return null;
}
} catch(e) { return null; }
}
// Seedance
async function seedanceVideo(prompt, imageUrl) {
if (!OPENROUTER_KEY) return null;
try {
const body = { model: currentVideoModel, prompt, aspect_ratio: '9:16', duration: 5, resolution: '720p' };
if (imageUrl) body.frame_images = [{ url: imageUrl, frame_type: 'first_frame' }];
const r = await fetch('https://openrouter.ai/api/v1/videos', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo', 'X-Title': 'maarmapa' },
body: JSON.stringify(body)
});
const text = await r.text();
console.log('Seedance:', text.slice(0, 150));
let d; try { d = JSON.parse(text); } catch(e) { return null; }
if (!d.id) return null;
const pollUrl = d.polling_url || ('https://openrouter.ai/api/v1/videos/' + d.id);
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 10000));
const t = await (await fetch(pollUrl, { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } })).json();
console.log('Seedance poll ' + i + ':', t.status);
if (t.status === 'completed') return 'https://openrouter.ai/api/v1/videos/' + d.id + '/content?index=0';
if (t.status === 'failed') return null;
}
return null;
} catch(e) { console.error('Seedance error:', e.message); return null; }
}
// ── v7.5: modelos dinámicos vía OpenRouter ────────────────────────────────
let orModelsCache = { at: 0, list: [] };
async function orModels() {
if (Date.now() - orModelsCache.at < 3600000 && orModelsCache.list.length) return orModelsCache.list;
try {
const r = await fetch('https://openrouter.ai/api/v1/models', { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } });
const d = await r.json();
orModelsCache = { at: Date.now(), list: d.data || [] };
} catch(e) { console.error('orModels:', e.message); }
return orModelsCache.list;
}
// busca el ID real por palabras clave (ej "seedance mini" → bytedance/seedance-x-mini)
async function resolveOR(hint) {
const toks = hint.toLowerCase().split(/[\s\-_.]+/).filter(Boolean);
const list = await orModels();
const hits = list.filter(m => { const id = (m.id + ' ' + (m.name || '')).toLowerCase(); return toks.every(t => id.includes(t)); });
if (!hits.length) return null;
hits.sort((a, b) => a.id.length - b.id.length);
return hits[0];
}
function modalidad(m) {
const out = ((m && m.architecture && m.architecture.output_modalities) || []).join(',');
if (out.includes('video')) return 'video';
if (out.includes('audio')) return 'audio';
if (out.includes('image')) return 'image';
return 'video';
}
async function orVideoGen(prompt, opts = {}) {
const body = { model: opts.model, prompt, aspect_ratio: opts.aspect || '9:16', duration: opts.duration || 5, resolution: opts.resolution || '720p' };
if (opts.imageUrl) body.frame_images = [{ url: opts.imageUrl, frame_type: 'first_frame' }];
const r = await fetch('https://openrouter.ai/api/v1/videos', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo', 'X-Title': 'maarmapa' },
body: JSON.stringify(body)
});
const text = await r.text();
console.log('orVideo', opts.model, ':', text.slice(0, 150));
let d; try { d = JSON.parse(text); } catch(e) { throw new Error(text.slice(0, 200)); }
if (!d.id) throw new Error((d.error && d.error.message) || text.slice(0, 200));
const pollUrl = d.polling_url || ('https://openrouter.ai/api/v1/videos/' + d.id);
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 10000));
const t = await (await fetch(pollUrl, { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } })).json();
console.log('orVideo poll ' + i + ':', t.status);
if (t.status === 'completed') return 'https://openrouter.ai/api/v1/videos/' + d.id + '/content?index=0';
if (t.status === 'failed') throw new Error((t.error && t.error.message) || 'generación falló');
}
throw new Error('timeout esperando el video');
}
async function orImageGen(prompt, model) {
const r = await fetch('https://openrouter.ai/api/v1/images/generations', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo', 'X-Title': 'maarmapa' },
body: JSON.stringify({ model, prompt, n: 1 })
});
const text = await r.text();
console.log('orImage', model, ':', text.slice(0, 150));
let d; try { d = JSON.parse(text); } catch(e) { throw new Error(text.slice(0, 200)); }
const img = d.data && d.data[0];
if (!img) throw new Error((d.error && d.error.message) || text.slice(0, 200));
if (img.url) return img.url;
if (img.b64_json) {
const buf = Buffer.from(img.b64_json, 'base64');
const r2 = await uploadToR2(buf, model.split('/').pop() + '_' + Date.now() + '.png', 'image/png');
if (r2) return r2;
}
throw new Error('sin imagen en la respuesta');
}
async function orAudioGen(prompt, model) {
const r = await fetch('https://openrouter.ai/api/v1/audio/generations', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo', 'X-Title': 'maarmapa' },
body: JSON.stringify({ model, prompt })
});
const text = await r.text();
console.log('orAudio', model, ':', text.slice(0, 150));
let d; try { d = JSON.parse(text); } catch(e) { throw new Error(text.slice(0, 200)); }
const a = (d.data && d.data[0]) || d;
if (a.url) return a.url;
const b64 = a.b64_json || a.audio;
if (b64) {
const buf = Buffer.from(b64, 'base64');
const r2 = await uploadToR2(buf, 'seedaudio_' + Date.now() + '.mp3', 'audio/mpeg');
if (r2) return r2;
}
throw new Error((d.error && d.error.message) || 'sin audio en la respuesta');
}
// comando genérico: resuelve el modelo real y llama según su modalidad
async function runORModel(chatId, hint, prompt, opts = {}) {
const msgId = await send(chatId, '🧪 Buscando modelo "' + hint + '" en OpenRouter...');
const m = await resolveOR(hint);
if (!m) { await edit(chatId, msgId, '❌ Ningún modelo calza con "' + hint + '". Usa /probemodelos para ver los disponibles.'); return; }
const kind = opts.kind || modalidad(m);
await edit(chatId, msgId, '🧪 *' + m.id + '* (' + kind + (opts.resolution ? ' ' + opts.resolution : '') + ')\n_generando..._');
try {
if (kind === 'video') {
const url = await orVideoGen(prompt, Object.assign({}, opts, { model: m.id }));
await video(chatId, url, '🎬 ' + m.id);
} else if (kind === 'image') {
const url = await orImageGen(prompt, m.id);
await photo(chatId, url, '🖼 ' + m.id);
} else {
const url = await orAudioGen(prompt, m.id);
await send(chatId, '🎵 *' + m.id + '*\n' + url);
}
await edit(chatId, msgId, '✅ *' + m.id + '* listo');
} catch(e) {
await edit(chatId, msgId, '❌ *' + m.id + '*: ' + String(e.message).slice(0, 300));
}
}
// inventario: qué modelos generativos hay en OpenRouter + qué acepta la API de Runway
async function probeModelos(chatId) {
const msgId = await send(chatId, '🔬 Consultando catálogos...');
const list = await orModels();
const interesantes = list.filter(m => /seedance|seedream|seed[-_ ]?audio|omni|veo|kling|runway|sora|wan/i.test(m.id));
const porTipo = {};
interesantes.forEach(m => { const k = modalidad(m); (porTipo[k] = porTipo[k] || []).push(m.id); });
let out = '🔬 *OpenRouter — modelos generativos (' + interesantes.length + '):*\n';
for (const k of Object.keys(porTipo)) out += '\n*' + k + ':*\n' + porTipo[k].map(i => '`' + i + '`').join('\n') + '\n';
try {
const r = await fetch('https://api.dev.runwayml.com/v1/text_to_image', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({ model: '__list__', promptText: 'x', ratio: '1920:1080' })
});
out += '\n*Runway text\\_to\\_image dice:* ' + (await r.text()).replace(/[*_`]/g, '').slice(0, 350);
} catch(e) {}
try {
const r2 = await fetch('https://api.dev.runwayml.com/v1/image_to_video', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({ model: '__list__', promptImage: 'https://map-ai-portfolio.vercel.app/portfolio_assets/thumbs/v01.jpg', promptText: 'x', ratio: '720:1280' })
});
out += '\n\n*Runway image\\_to\\_video dice:* ' + (await r2.text()).replace(/[*_`]/g, '').slice(0, 350);
} catch(e) {}
await edit(chatId, msgId, out.slice(0, 3900));
}
// DeepSeek
async function deepseek(prompt, system) {
if (!OPENROUTER_KEY) { console.error('deepseek: OPENROUTER_KEY no configurada'); return null; }
try {
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo' },
body: JSON.stringify({ model: currentTextModel, max_tokens: 4096, messages: [...(system ? [{ role: 'system', content: system }] : []), { role: 'user', content: prompt }] }),
signal: AbortSignal.timeout(60000)
});
const data = await r.json();
if (data.error) { console.error('deepseek API error:', JSON.stringify(data.error)); return null; }
return data.choices?.[0]?.message?.content || null;
} catch(e) { console.error('deepseek fetch error:', e.message); return null; }
}
// Wake agent
async function wakeAgent(chatId, msgId) {
for (let i = 0; i < 10; i++) {
try {
const t = await (await fetch(AGENT_URL + '/')).text();
if (t.includes('maarmapa agent')) return true;
} catch(e) {}
await edit(chatId, msgId, '🏭 *factory*\n' + bar(1, 10) + '\n_Despertando agente ' + (i+1) + '/10..._');
await new Promise(r => setTimeout(r, 8000));
}
return false;
}
// POST FACTORY
async function runFactory(chatId, topic, run = null) {
// Sin `run` es una corrida nueva; con `run` venimos de /retry y los pasos
// que ya salieron bien se saltan (no se vuelven a pagar).
if (!run) run = runs.startRun({ chatId, kind: '/post', topic });
const msgId = await send(chatId, '🏭 *maarmapa factory*\n' + bar(0, 10) + '\n_Iniciando..._ `' + run.id + '`');
const awake = await wakeAgent(chatId, msgId);
if (!awake) { await edit(chatId, msgId, '❌ Agente no responde. Intenta en 1 minuto.'); return; }
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(1, 10) + '\n_Buscando contenido..._');
const postData = await runs.step(run, 'post', async () => {
const res = await fetch(AGENT_URL + '/post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, search_official: true, include_images: true })
});
const r = await res.json();
return r.post || r;
});
if (!postData) {
runs.finishRun(run);
await edit(chatId, msgId, '❌ Error: ' + run.steps['post'].error + '\n\nReintentar: `/retry ' + run.id + '`');
return;
}
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(2, 10) + '\n_Generando narrativa visual..._');
const title = (postData.title || topic).slice(0, 120);
const body = (postData.body || '').replace(/<cite[^>]*>[\s\S]*?<\/cite>/gi, '').replace(/#{1,3} [^\n]+\n?/g, '\n').replace(/\*\*/g, '').trim().slice(0, 900);
const caption = (postData.instagram_caption || '').slice(0, 250);
const paragraphs = body.split('\n\n').filter(p => p.length > 20);
const p1 = paragraphs[0] || body;
const p2 = paragraphs[1] || p1;
const dataPoints = body.match(/\d+%?|\d+\.\d+[a-z]*/g) || [];
const stat = dataPoints[0] || '∞';
await send(chatId, '📝 *' + title + '*\n\n' + body + '...');
if (caption) await send(chatId, '📌 _Caption:_\n' + caption);
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(3, 10) + '\n_🖼 Thumbnail..._');
const thumbPrompt = postData.thumbnail_prompt || ('Dark editorial cinematic. Topic: ' + title + '. Moody atmospheric urban contemporary art. Black background. Dramatic lighting. High contrast.');
const thumbUrl = await runs.step(run, 'thumb', async () => {
const u = await grokImg(thumbPrompt);
if (!u) throw new Error('Grok no devolvió thumbnail');
return u;
});
if (thumbUrl) await photo(chatId, thumbUrl, '🖼 Thumbnail');
const T = title.toUpperCase().slice(0, 38);
const style = postData.visual_style || 'dark editorial cinematic';
const coherentPrompts = [
'Square 1:1 editorial Instagram. Dark #080808. Safe zone 120px. Cinematic ' + style + '. Bebas Neue white massive: "' + T + '". 01/07. Ghost vignette 12%. INTRO SLIDE.',
'Square 1:1 editorial Instagram. White #f5f5f0. Safe zone 120px. Bebas Neue black bold. Key insight: "' + p1.slice(0, 60) + '". 02/07. CONTEXT.',
'Square 1:1 editorial Instagram. Dark. Safe zone 120px. ' + style + '. Vertical accent line left. Massive: "' + stat + '". 03/07. DATA.',
'Square 1:1 editorial Instagram. Dark. Safe zone 120px. Quotation mark 3%. Italic serif light gray: "' + p2.slice(0, 50) + '...". 04/07. VOICE.',
'Square 1:1 editorial Instagram. Dark. Safe zone 120px. 2x2 brutalist grid. Bold white 4 key concepts from "' + title + '". 05/07.',
'Square 1:1 editorial Instagram. White #f5f5f0. Safe zone 120px. Centered Bebas Neue: provocative question about "' + title + '". 06/07.',
'Square 1:1 editorial Instagram. Dark. Safe zone 120px. Urban pattern 5%. ' + style + '. @maarmapa.eth. 07/07. CLOSURE.'
];
// Array de 7 posiciones FIJAS. Antes era un push: si el slide 3 fallaba,
// el 4 se corría al índice 3 y más abajo recibía motions[3] — el movimiento
// equivocado. Con posiciones fijas el índice siempre significa lo mismo.
const slides = new Array(7).fill(null);
for (let i = 0; i < 7; i++) {
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(4 + i, 10) + '\n_📸 Slide ' + (i + 1) + '/7..._');
const url = await runs.step(run, 'slide-' + i, async () => {
const u = await grokImg(coherentPrompts[i]);
if (!u) throw new Error('Grok no devolvió imagen');
return u;
});
if (url) { slides[i] = url; await photo(chatId, url, 'Slide ' + (i + 1) + '/7'); }
}
const slideUrls = slides.filter(Boolean);
let clips = 0;
if (process.env.RUNWAY_KEY && slideUrls.length > 0) {
const motions = [
'INTRO. Dark ' + style + '. Title slides left. Slow cinematic fade-in.',
'REVELATION. White slide. Text scales up with impact.',
'EVIDENCE. Numbers animate upward. Vertical line accent.',
'REFLECTION. Quote drifts like smoke. Contemplative pause.',
'EXPANSION. Grid cells flash rhythm. Brutalist power.',
'PROVOCATION. Letters explode outward. Kinetic energy.',
'RESOLUTION. Fade to deep black. @maarmapa.eth sharp.'
];
for (let i = 0; i < 7; i++) {
if (!slides[i]) continue; // sin slide no hay nada que animar
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(9, 10) + '\n_🎬 Clip ' + (i + 1) + '/7..._');
const vid = await runs.step(run, 'clip-' + i, async () => {
const v = await runwayVideo(slides[i], motions[i], 4);
if (!v) throw new Error('Runway no devolvió video');
return v;
});
if (vid) { await video(chatId, vid, '🎬 Clip ' + (i + 1)); clips++; }
}
}
runs.finishRun(run);
const fallidos = runs.failedSteps(run);
await edit(chatId, msgId, '🏭 *maarmapa factory*\n' + bar(10, 10) +
(fallidos.length ? '\n⚠️ *Parcial*' : '\n✅ *Completado*'));
let resumen = (fallidos.length ? '⚠️ *Parcial*' : '✅ *Listo*') +
'\n📸 Slides: ' + slideUrls.length + '/7\n🎬 Clips: ' + clips;
if (fallidos.length) {
// Antes esto era silencio: se veía "Slides: 6" y nunca cuál faltó.
resumen += '\n\n*Fallaron ' + fallidos.length + ' pasos:*\n' +
fallidos.map(f => '• `' + f.id + '` — ' + f.error).join('\n') +
'\n\nRetomar solo lo que falta:\n`/retry ' + run.id + '`';
}
await send(chatId, resumen);
}
// ANIME FACTORY
async function runAnime(chatId, concept) {
const msgId = await send(chatId, '🎬 *anime factory*\n' + bar(0, 10) + '\n_Iniciando..._');
const BS = BASE_STYLE + ' ' + CITY_BG + ' 9:16 vertical ALL 120px safe margins.';
const chars = [
{ character: 'Andino', role: 'Beatmaker', prompt: BS + ' ' + ANDINO_P + ' Full body character sheet front 3/4. White background.' },
{ character: 'Piero', role: 'MC', prompt: BS + ' ' + PIERO_P + ' Full body character sheet front 3/4. White background.' },
{ character: 'Kinny', role: 'Dancer', prompt: BS + ' ' + KINNY_P + ' Full body visible. White background.' }
];
const scenePrompts = [
BS + ' SHOT 1. ' + ANDINO_P + ' Rooftop 360 orbit low angle. Red lightning rain. Andes silhouette.',
BS + ' SHOT 2. ' + PIERO_P + ' + ' + KINNY_P + ' Alley 180 arc. Steam grates. Kanji graffiti.',
BS + ' SHOT 3. ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' Triangle top-down. SOUTHSIDE red glitch text. Yin yang glowing.'
];
const motions = [
'Akira anime 360 orbit rising low angle. Red lightning. Rain streaks.',
'Dynamic 180 arc. Dancer spin motion blur. MC raises mic. Steam jets.',
'Top-down drone descent fast. Energy burst expands. White flash freeze.'
];
await send(chatId, '🎨 *Squad:*\n1. *Andino* — Beatmaker\n2. *Piero* — MC\n3. *Kinny* — Dancer');
for (let i = 0; i < chars.length; i++) {
await edit(chatId, msgId, '🎬 *anime factory*\n' + bar(2+i, 10) + '\n_🎨 ' + chars[i].character + '..._');
const u = await grokImg(chars[i].prompt);
if (u) await photo(chatId, u, '🎨 ' + chars[i].character + ' — ' + chars[i].role);
}
const clips = [];
for (let i = 0; i < 3; i++) {
await edit(chatId, msgId, '🎬 *anime factory*\n' + bar(5+i, 10) + '\n_🎨 Shot ' + (i+1) + '/3..._');
const su = await grokImg(scenePrompts[i]);
if (su) {
await photo(chatId, su, '🎬 Shot ' + (i+1) + '/3');
await edit(chatId, msgId, '🎬 *anime factory*\n' + bar(6+i, 10) + '\n_🎬 Runway Shot ' + (i+1) + '..._');
const vid = await runwayVideo(su, motions[i], 5);
if (vid) { clips.push(vid); await video(chatId, vid, '🎬 Shot ' + (i+1) + '/3'); }
}
}
await edit(chatId, msgId, '🎬 *anime factory*\n' + bar(10, 10) + '\n✅ *Completado*');
await send(chatId, '✅ *Anime listo*\n🎬 Clips: ' + clips.length + '/3');
}
// SQUAD MULTI-ANGLE
async function runSquad(chatId) {
const msgId = await send(chatId, '🥷 *SQUAD factory*\n' + bar(0, 10) + '\n_Iniciando..._');
const BS = BASE_STYLE + ' ' + CITY_BG + ' 9:16 vertical ALL 120px safe margins.';
const angles = [
{ label: 'Kinny — Action', prompt: BS + ' ' + KINNY_P + ' FROZEN MID-AIR right leg full kick. THREE shuriken orbit. Speed lines all. Blue energy trails.', motion: 'Kinny mid-air kick ULTRA SLOW MOTION. Shuriken scatter. Teal energy explosion. Camera 180 arc.' },
{ label: 'Kinny — Low Angle', prompt: BS + ' ' + KINNY_P + ' EXTREME LOW ANGLE. Wide warrior stance both arms raised. Blue corona shockwave. Rain.', motion: 'Camera rises from ground. Blue corona expands. Shuriken orbit accelerates. Blood moon blazes.' },
{ label: 'Kinny — Portrait', prompt: BS + ' ' + KINNY_P + ' WAIST UP 3/4. Right hand spinning shuriken eye level. Blue energy crackling. Single neon blue light from left.', motion: 'Shuriken spins in slow motion. Blue energy crackles around fist. Camera slow push-in to eyes.' },
{ label: 'Andino — Beat', prompt: BS + ' ' + ANDINO_P + ' MEDIUM SHOT low angle. Both hands MPC mid-strike. Red energy pulses. Face bowed. Crimson particles float up.', motion: 'MPC pad strikes send red shockwaves through stone. Crimson particles float upward. Camera circles slowly.' },
{ label: 'Andino — Rooftop', prompt: BS + ' ' + ANDINO_P + ' FULL BODY rooftop edge. Right arm raised vinyl disc glowing red. Left arm balance. City below. Red storm sky.', motion: 'Camera slow push-in. Wind presses suit. Crimson headphones pulse. Direct eye contact.' },
{ label: 'Piero — Battle', prompt: BS + ' ' + PIERO_P + ' HERO SHOT center. Microphone thrust toward camera gold energy beam. Palm strike. Wet cobblestones.', motion: 'Gold energy beam expands. Stone walls crack. Rain drops freeze in shockwave. Camera rapid push-in.' },
{ label: 'Squad — Final', prompt: BS + ' EPIC WIDE. ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' Three coronas red gold teal merge white. Yin yang ground. SOUTHSIDE red glitch top.', motion: 'All three advance slow motion. Combined energy field. Massive white explosion. Freeze. SOUTHSIDE burns.' }
];
const clips = [];
for (let i = 0; i < angles.length; i++) {
await edit(chatId, msgId, '🥷 *SQUAD factory*\n' + bar(i+1, angles.length+1) + '\n_🎨 ' + angles[i].label + '..._');
const u = await grokImg(angles[i].prompt);
if (u) {
await photo(chatId, u, '🎨 ' + angles[i].label);
await edit(chatId, msgId, '🥷 *SQUAD factory*\n' + bar(i+1, angles.length+1) + '\n_🎬 Runway: ' + angles[i].label + '..._');
const vid = await runwayVideo(u, angles[i].motion, 5);
if (vid) {
await video(chatId, vid, '🎬 ' + angles[i].label);
try {
const vr = await fetch(vid);
const vb = await vr.arrayBuffer();
const r2u = await uploadToR2(vb, 'squad_' + i + '_' + Date.now() + '.mp4', 'video/mp4');
if (r2u) saveClip(chatId, r2u);
} catch(e) { console.error('R2 upload (squad) failed:', e.message); }
clips.push(vid);
}
}
}
await edit(chatId, msgId, '🥷 *SQUAD factory*\n' + bar(10, 10) + '\n✅ *Completado*');
await send(chatId, '✅ *Squad listo*\n🎨 ' + angles.length + ' imágenes\n🎬 ' + clips.length + ' clips\n_Usa /sync para mezclar con SOUTHSIDE_');
}
// SEEDANCE SCENES
const SCENES = {
'andino-intro': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Low angle. MPC altar red runes. Blood moon broken arch. CAMERA rises. 9:16 ALL 120px.',
'andino-battle': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Both hands MPC mid-strike. Red energy beams. Elevated ruins. CAMERA pulls back. 9:16 ALL 120px.',
'andino-ritual': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Kneeling MPC center yin-yang stone floor. Red energy stone veins. Blood moon. Top-down. 9:16 ALL 120px.',
'andino-finale': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Standing rooftop edge. Arms crossed. MPC at feet. Blood moon blazing. 9:16 ALL 120px.',
'andino-street': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Walking toward camera rain-soaked alley. MPC under arm. Colonial archway behind. 9:16 ALL 120px.',
'andino-name': 'Wu-Tang dark anime. ANDINO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Full body dramatic low angle. Massive bold distressed glitch typography spelling exactly ANDINO in blood crimson red — same exact style as SOUTHSIDE title card — positioned in UPPER THIRD above character. Blood moon. 9:16 ALL 120px.',
'piero-intro': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Stepping from darkness into candlelight. Iron microphone raised. Gold smoke tip. Stone corridor. 9:16 ALL 120px.',
'piero-battle': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Battle stance rain alley. Mic thrust forward. Gold energy beam. Shockwave cracking walls. 9:16 ALL 120px.',
'piero-ritual': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Center stone circle. Mic ceremonial staff. Gold energy from blood moon through mic into stone. 9:16 ALL 120px.',
'piero-finale': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Facing camera. Mic raised toward lens. Massive gold corona above. 9:16 ALL 120px.',
'piero-street': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Leaning graffiti wall broken lamplight. Mic hanging. Eyes sharp watching. 9:16 ALL 120px.',
'piero-name': 'Wu-Tang dark anime. PIERO solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Full body center frame. Gold corona above. THE TEXT MUST READ EXACTLY: first line "PIERO" second line "LA ROCCA" spelled P-I-E-R-O and L-A-R-O-C-C-A in blood crimson red same style as SOUTHSIDE title card UPPER THIRD above character. 9:16 ALL 120px.',
'kinny-intro': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Drops from above landing Shaolin crouch wet stone. Three shuriken appear orbiting. Teal energy pulses. 9:16 ALL 120px.',
'kinny-battle': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' FROZEN MID-AIR peak spinning kick. Leg fully extended. THREE shuriken orbit. Teal corona blazing. 9:16 ALL 120px.',
'kinny-ritual': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Kneeling stone circle. Three shuriken as offerings. Teal pulsing. Blood moon. Top-down. 9:16 ALL 120px.',
'kinny-finale': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Facing camera. Three shuriken orbiting. Weight forward coiled. Massive teal energy building. 9:16 ALL 120px.',
'kinny-street': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Drops rooftop wet Santiago cobblestones. Steam surrounds. Low angle. Andes behind. 9:16 ALL 120px.',
'kinny-name': 'Wu-Tang dark anime. KINNY solo. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Mid-air frozen kick. Three shuriken orbiting. Teal energy blazing. Massive bold distressed glitch typography spelling exactly IlKINNY in blood crimson red — same exact style as SOUTHSIDE title card — UPPER THIRD above character. 9:16 ALL 120px.',
'squad-name': 'Wu-Tang dark anime. ' + BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' All three triangle formation advancing toward camera. Three energy coronas red gold teal merging white center. Blood moon blazing. Yin yang erupting. No text no typography. Pure cinematic power. 9:16 ALL 120px.',
intro: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' All three emerging from darkness. Blood moon. Yin yang ground. SOUTHSIDE red glitch. 9:16 ALL 120px.',
battle: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' All three in battle action. Energy beams red gold teal crossing. SOUTHSIDE. 9:16 ALL 120px.',
ritual: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' Triangle yin yang stone floor blood moon. Three coronas merging. SOUTHSIDE. 9:16 ALL 120px.',
finale: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' All three advancing toward camera. Massive combined energy burst. SOUTHSIDE. 9:16 ALL 120px.',
street: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' ' + ANDINO_P + ' ' + PIERO_P + ' ' + KINNY_P + ' Santiago alley 3am all three emerging from steam. Colonial archway. Andes. Blood moon. SOUTHSIDE. 9:16 ALL 120px.'
};
async function runSeedance(chatId, concept) {
const msgId = await send(chatId, '🌱 *Seedance factory*\n' + bar(0, 10) + '\n_Iniciando..._');
if (!OPENROUTER_KEY) { await edit(chatId, msgId, '❌ OPENROUTER_KEY no configurada.'); return; }
const sceneKey = concept?.toLowerCase().trim();
const prompt = SCENES[sceneKey] || concept || SCENES.intro;
await edit(chatId, msgId, '🌱 *Seedance factory*\n' + bar(2, 10) + '\n_🎨 Generando frame referencia..._');
const refImg = await grokImg(prompt.slice(0, 500));
if (refImg) await photo(chatId, refImg, '🎨 Frame referencia');
await edit(chatId, msgId, '🌱 *Seedance factory*\n' + bar(4, 10) + '\n_🎬 ' + currentVideoModel.split('/')[1] + ' generando..._');
const vid = await seedanceVideo(prompt, null);
if (vid) {
await edit(chatId, msgId, '🌱 *Seedance factory*\n' + bar(8, 10) + '\n_📥 Descargando y subiendo a R2..._');
try {
const vr = await fetch(vid, { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } });
const vb = await vr.arrayBuffer();
const filename = 'seedance_' + (sceneKey || 'clip') + '_' + Date.now() + '.mp4';
const r2u = await uploadToR2(vb, filename, 'video/mp4');
const clipUrl = r2u || vid;
saveClip(chatId, clipUrl);
const form = new FormData();
form.append('chat_id', String(chatId));
form.append('video', new Blob([vb], { type: 'video/mp4' }), filename);
form.append('caption', '🎬 Seedance — ' + (sceneKey || 'clip') + (r2u ? ' ✅ R2' : ''));
await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/sendVideo', { method: 'POST', body: form });
await edit(chatId, msgId, '🌱 *Seedance factory*\n' + bar(10, 10) + '\n✅ *Completado — guardado para /sync*');
} catch(e) { await edit(chatId, msgId, '✅ Video listo: ' + vid); }
} else {
await edit(chatId, msgId, '❌ No generó video. Verifica créditos en openrouter.ai');
}
}
// RUNWAY SCENES
const RUNWAY_SCENES = {
'andino-intro': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Low angle. MPC altar red runes. Blood moon broken arch. 9:16 ALL 120px.', motion: 'Camera rises slowly from ground toward ANDINO. Red runes pulse. Black smoke drifts upward. Blood moon intensifies.' },
'andino-battle': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Both hands MPC mid-strike. Red energy beams. Elevated broken ruins. 9:16 ALL 120px.', motion: 'Camera pulls back revealing full scale. MPC strikes send red shockwaves. Debris swirls. Crimson light pulses.' },
'andino-ritual': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Kneeling MPC center yin-yang stone floor. Red energy through stone veins. Blood moon. Top-down. 9:16 ALL 120px.', motion: 'Top-down camera descends slowly. Red energy pulses through stone carvings. Yin-yang brightens. Ancient ritual builds.' },
'andino-finale': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Standing rooftop edge. Arms crossed. MPC at feet. Crimson headphones glowing. Blood moon. 9:16 ALL 120px.', motion: 'Camera slow push-in. Wind presses suit. Crimson headphones pulse. Direct eye contact through mask. Absolute stillness.' },
'andino-street': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + ANDINO_P + ' Walking toward camera rain-soaked alley. MPC under arm. Steam. Colonial archway. 9:16 ALL 120px.', motion: 'Long lens compression ANDINO walks toward camera. Each step red ripple through puddles. Steam parts. Unstoppable.' },
'piero-intro': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Stepping from darkness into candlelight. Iron microphone raised. Gold smoke from tip. Stone corridor. 9:16 ALL 120px.', motion: 'PIERO materializes from pure darkness. Gold smoke rises from mic tip. Camera slow push-in. Voice of the clan.' },
'piero-battle': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Battle stance rain alley. Mic thrust forward. Gold energy beam. Shockwave cracking walls. 9:16 ALL 120px.', motion: 'Gold energy beam expands with force. Stone walls crack. Rain freezes in shockwave. Camera rapid push-in to eyes.' },
'piero-ritual': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Center stone circle. Mic as ceremonial staff. Gold energy from blood moon through mic into stone. 9:16 ALL 120px.', motion: 'Gold light from blood moon through PIERO into mic. Stone carvings illuminate gold outward. Camera circles slowly.' },
'piero-finale': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Facing camera. Mic raised toward lens. Gold corona forming above. 9:16 ALL 120px.', motion: 'PIERO raises mic directly at camera slow motion. Gold corona expands massively. Camera pushed back by force.' },
'piero-street': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + PIERO_P + ' Leaning graffiti wall broken lamplight. Mic hanging. Eyes watching. 9:16 ALL 120px.', motion: 'PIERO pushes off wall fluid motion toward camera. Gold builds around mic each step. Gold shockwave ripples.' },
'kinny-intro': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Drops from above landing Shaolin crouch wet stone. Three shuriken appear orbiting. Teal energy pulses. 9:16 ALL 120px.', motion: 'KINNY drops into frame lands in stillness. Three shuriken materialize orbit in sequence. Teal energy pulses. Coiled readiness.' },
'kinny-battle': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' FROZEN mid-air peak spinning kick. Leg fully extended. THREE shuriken orbit. Teal corona blazing. 9:16 ALL 120px.', motion: 'Time resumes ULTRA SLOW MOTION. Leg completes arc motion blur. Shuriken scatter. Teal explosion. Camera 180 rotation.' },
'kinny-ritual': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Kneeling stone circle. Three shuriken as offerings. Teal pulsing. Blood moon. Top-down. 9:16 ALL 120px.', motion: 'Teal energy pulses through KINNY breathing rhythm. Shuriken rotate like satellites. He stands arms raised. Teal corona explodes.' },
'kinny-finale': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Facing camera. Three shuriken orbiting. Weight forward. Direct eye contact. Massive teal energy building. 9:16 ALL 120px.', motion: 'One slow step forward. Shuriken orbit accelerates. Teal corona expands. Camera pushed backward. Massive teal explosion freeze.' },
'kinny-street': { img: BASE_STYLE + ' ' + CITY_BG + ' ' + KINNY_P + ' Drops rooftop wet Santiago cobblestones. Steam surrounds. Low angle. Andes behind. 9:16 ALL 120px.', motion: 'Landing teal shockwave ripples. Camera looks up as KINNY rises. Shuriken orbit. Walks through steam toward camera.' },
intro: { img: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' All three emerging from darkness. Blood moon. SOUTHSIDE. 9:16 ALL 120px.', motion: 'All three materialize from darkness. Energy coronas ignite. Camera pulls back. Blood moon blazes. SOUTHSIDE burns.' },
battle: { img: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' All three battle simultaneously. Red gold teal crossing. SOUTHSIDE. 9:16 ALL 120px.', motion: 'All three unleash simultaneously. Red gold teal energy beams cross center. Camera pulls back fast. Pure power.' },
ritual: { img: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' Triangle yin yang floor blood moon. Three coronas merging. 9:16 ALL 120px.', motion: 'Three coronas merge into white light. Yin yang expands. Camera descends from above. Ancient ritual complete.' },
finale: { img: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' All three advancing camera. Combined energy burst. SOUTHSIDE. 9:16 ALL 120px.', motion: 'All three advance slow motion. Combined energy pushes camera back. Massive white explosion. Freeze. SOUTHSIDE.' },
street: { img: BASE_STYLE + ' ' + CITY_BG + ' ' + SQUAD_COMP + ' Santiago alley 3am steam. Colonial archway. 9:16 ALL 120px.', motion: 'All three emerge from steam walk toward camera through rain. Each step colored energy ripple. Camera orbits 360.' }
};
async function runRunwayScene(chatId, sceneKey) {
const msgId = await send(chatId, '🎬 *Runway factory*\n' + bar(0, 10) + '\n_Escena: ' + sceneKey + '..._');
const scene = RUNWAY_SCENES[sceneKey];
if (!scene) { await edit(chatId, msgId, '❌ Escena no encontrada. Usa `/runway` para ver opciones.'); return; }
await edit(chatId, msgId, '🎬 *Runway factory*\n' + bar(3, 10) + '\n_🎨 Grok generando imagen..._');
const imgUrl = await grokImg(scene.img);
if (!imgUrl) { await edit(chatId, msgId, '❌ Grok no generó imagen.'); return; }
await photo(chatId, imgUrl, '🎨 Frame: ' + sceneKey);
await edit(chatId, msgId, '🎬 *Runway factory*\n' + bar(6, 10) + '\n_🎬 Runway animando..._');
const vid = await runwayVideo(imgUrl, scene.motion, 5);
if (vid) {
await video(chatId, vid, '🎬 Runway — ' + sceneKey);
await edit(chatId, msgId, '🎬 *Runway factory*\n' + bar(9, 10) + '\n_☁️ Subiendo a R2..._');
try {
const vr = await fetch(vid);
const vb = await vr.arrayBuffer();
const filename = 'runway_' + sceneKey.replace('-', '_') + '_' + Date.now() + '.mp4';
const r2u = await uploadToR2(vb, filename, 'video/mp4');
if (r2u) saveClip(chatId, r2u);
} catch(e) { saveClip(chatId, vid); }
await edit(chatId, msgId, '🎬 *Runway factory*\n' + bar(10, 10) + '\n✅ *Completado — guardado para /sync*');
} else {
await edit(chatId, msgId, '❌ Runway no generó el clip.');
}
}
// SHOTSTACK SYNC — BPM 103
async function runSync(chatId, clipUrls) {
const msgId = await send(chatId, '🎵 *Sync factory*\n' + bar(0, 10) + '\n_Iniciando..._');
const SHOTSTACK_KEY = process.env.SHOTSTACK_KEY;
if (!SHOTSTACK_KEY) { await edit(chatId, msgId, '❌ SHOTSTACK_KEY no configurada.'); return; }
if (!clipUrls || clipUrls.length === 0) { await edit(chatId, msgId, '❌ No hay clips. Usa /syncr2 primero.'); return; }
await edit(chatId, msgId, '🎵 *Sync factory*\n' + bar(3, 10) + '\n_BPM 103 sync con ' + clipUrls.length + ' clips..._');
const BPM = 103;
const beat = 60 / BPM;
const bar4 = beat * 4;
const bar8 = beat * 8;
const DROP = 2.0;
const TRIM = 2.0;
const MAX_CLIP = 4.5;
const durations = clipUrls.map((_, i) => {
if (i === 0) return Math.min(DROP, MAX_CLIP);
if (i === clipUrls.length - 1) return Math.min(bar8, MAX_CLIP);
return Math.min(bar4, MAX_CLIP);
});
let t = 0;
const starts = durations.map(d => { const s = t; t += d; return parseFloat(s.toFixed(3)); });
const timeline = {
soundtrack: { src: SOUTHSIDE_AUDIO, volume: 1 },
tracks: [{
clips: clipUrls.map((url, i) => ({
asset: { type: 'video', src: url, volume: 0, trim: i === 0 ? 0 : TRIM },
start: starts[i],
length: parseFloat(durations[i].toFixed(3)),
fit: 'crop',
transition: { in: i === 0 ? 'fadeFast' : 'none', out: i === clipUrls.length - 1 ? 'fadeFast' : 'none' }
}))
}],
background: '#000000'
};
try {
const sr = await fetch('https://api.shotstack.io/edit/stage/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': SHOTSTACK_KEY },
body: JSON.stringify({ timeline, output: { format: 'mp4', resolution: 'hd', aspectRatio: '9:16', fps: 25 } })
});
const sd = await sr.json();
if (!sd.response?.id) { await edit(chatId, msgId, '❌ Shotstack error: ' + JSON.stringify(sd).slice(0, 200)); return; }
const renderId = sd.response.id;
await edit(chatId, msgId, '🎵 *Sync factory*\n' + bar(5, 10) + '\n_Renderizando..._');
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const pr = await fetch('https://api.shotstack.io/edit/stage/render/' + renderId, { headers: { 'x-api-key': SHOTSTACK_KEY } });
const pd = await pr.json();
const status = pd.response?.status;
const url = pd.response?.url;
await edit(chatId, msgId, '🎵 *Sync factory*\n' + bar(5 + Math.min(i, 4), 10) + '\n_Renderizando ' + (i*10) + 's..._');
if (status === 'done' && url) {
try {
const vr = await fetch(url);
const vb = await vr.arrayBuffer();
const form = new FormData();
form.append('chat_id', String(chatId));
form.append('video', new Blob([vb], { type: 'video/mp4' }), 'southside_final.mp4');
form.append('caption', '🎵 SOUTHSIDE — ' + clipUrls.length + ' clips BPM 103');
await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/sendVideo', { method: 'POST', body: form });
} catch(e) { console.error('Send error:', e.message); }
await edit(chatId, msgId, '🎵 *Sync factory*\n' + bar(10, 10) + '\n✅ *Video final listo*');
return;
}
if (status === 'failed') { await edit(chatId, msgId, '❌ Shotstack render falló.'); return; }
}
await edit(chatId, msgId, '❌ Timeout.');
} catch(e) { await edit(chatId, msgId, '❌ Error: ' + e.message); }
}
// BOYKOT FACTORY
const BOYKOT_STYLE = 'Editorial Instagram for Boykot.cl Chilean art supply store. Brand: black background #000000 acid yellow-green #CCFF00 accent. Clean minimal product editorial. Urban art supplies. Target: artists illustrators muralists designers Chile.';
async function runBoykotPost(chatId, topic) {
const msgId = await send(chatId, '🎨 *Boykot factory*\n' + bar(0, 10) + '\n_Iniciando..._');
await edit(chatId, msgId, '🎨 *Boykot factory*\n' + bar(2, 10) + '\n_Generando contenido..._');
const system = 'Eres community manager Boykot.cl tienda materiales artisticos chilena. Genera contenido Instagram espanol tono cercano apasionado por el arte. Devuelve SOLO JSON: {"caption":"...","hashtags":"...","slide_prompts":["p1","p2","p3"]}';
let postData;
try {
const raw = await deepseek('Genera contenido Instagram Boykot.cl sobre: ' + topic, system);
if (raw) postData = JSON.parse(raw.replace(/```json|```/g, '').trim());
} catch(e) { postData = null; }
if (!postData) {
postData = {
caption: topic + ' — disponible en Boykot.cl',
hashtags: '#boykot #artesupplies #chile #arte #pintura #ilustracion #diseno',
slide_prompts: [
'Product hero ' + topic + ' black background #CCFF00 rim lighting. Minimal editorial.',
'Detail close-up ' + topic + ' macro. High contrast black #CCFF00.',
'Lifestyle dark studio artist using ' + topic + '. Chilean urban art. #CCFF00 accent.'
]
};
}
await edit(chatId, msgId, '🎨 *Boykot factory*\n' + bar(3, 10) + '\n_Copy listo_');
await send(chatId, '📝 *Caption Boykot:*\n\n' + postData.caption + '\n\n' + postData.hashtags);
const slides = postData.slide_prompts || [];
const carouselUrls = [];
for (let i = 0; i < Math.min(slides.length, 3); i++) {
await edit(chatId, msgId, '🎨 *Boykot factory*\n' + bar(4+i, 10) + '\n_📸 Carrusel ' + (i+1) + '/3..._');
const u = await grokImg('Square 1:1. Safe 100px all sides. ' + BOYKOT_STYLE + ' ' + slides[i] + ' ALL inside 100px. No watermarks.');
if (u) { carouselUrls.push(u); await photo(chatId, u, 'Carrusel ' + (i+1) + '/3 — Boykot.cl'); }
}
const reelUrls = [];
for (let i = 0; i < Math.min(slides.length, 3); i++) {
await edit(chatId, msgId, '🎨 *Boykot factory*\n' + bar(7+i, 10) + '\n_🎬 Reel ' + (i+1) + '/3..._');
const u = await grokImg('Vertical 9:16. Safe 120px all sides. ' + BOYKOT_STYLE + ' ' + slides[i] + ' ALL inside 120px. No watermarks.');
if (u) { reelUrls.push(u); await photo(chatId, u, 'Reel ' + (i+1) + '/3 — Boykot.cl'); }
}
let clips = 0;
if (process.env.RUNWAY_KEY && reelUrls.length > 0) {
const motions = ['Product reveal slow push-in. Neon yellow-green sweeps surface. Commercial editorial.', 'Detail zoom macro. Acid yellow highlight traces edge. Professional.', 'Lifestyle artist hand motion blur. Yellow-green neon glow pulses.'];
for (let i = 0; i < reelUrls.length; i++) {
const vid = await runwayVideo(reelUrls[i], motions[i], 3);
if (vid) { await video(chatId, vid, 'Reel clip ' + (i+1) + ' Boykot.cl'); clips++; }
}
}
await edit(chatId, msgId, '🎨 *Boykot factory*\n' + bar(10, 10) + '\n✅ *Completado*');
await send(chatId, '✅ *Boykot listo*\n📸 Carrusel: ' + carouselUrls.length + '/3\n🎬 Reel frames: ' + reelUrls.length + '/3\n🎥 Clips: ' + clips);
}
// === SEEDANCE 16:9 (horizontal) ===
async function seedanceVideo16(prompt) {
if (!OPENROUTER_KEY) return null;
try {
const promptH = prompt.replace(/9:16[^.]*/gi, '16:9 horizontal cinematic widescreen, wide shot, the ruined environment fills both sides of the frame');
const body = { model: currentVideoModel, prompt: promptH, aspect_ratio: '16:9', duration: 5, resolution: '720p' };
const r = await fetch('https://openrouter.ai/api/v1/videos', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + OPENROUTER_KEY, 'HTTP-Referer': 'https://maarmapa.eth.limo', 'X-Title': 'maarmapa' },
body: JSON.stringify(body)
});
const text = await r.text(); console.log('Seedance16:', text.slice(0,150));
let d; try { d = JSON.parse(text); } catch(e) { return null; }
if (!d.id) return null;
const pollUrl = d.polling_url || ('https://openrouter.ai/api/v1/videos/' + d.id);
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 10000));
const t = await (await fetch(pollUrl, { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } })).json();
if (t.status === 'completed') return 'https://openrouter.ai/api/v1/videos/' + d.id + '/content?index=0';
if (t.status === 'failed') return null;
}
return null;
} catch(e) { console.error('Seedance16 error:', e.message); return null; }
}
async function runSeedance16(chatId, concept) {
const msgId = await send(chatId, '🌅 *Seedance 16:9*\n' + bar(0,10) + '\n_Generando horizontal..._');
if (!OPENROUTER_KEY) { await edit(chatId, msgId, '❌ OPENROUTER_KEY no configurada.'); return; }
const sceneKey = concept?.toLowerCase().trim();
const prompt = SCENES[sceneKey] || concept || SCENES.intro;
const vid = await seedanceVideo16(prompt);
if (vid) {
try {
const vb = await (await fetch(vid, { headers: { 'Authorization': 'Bearer ' + OPENROUTER_KEY } })).arrayBuffer();
const filename = 'seedance16_' + (sceneKey || 'clip') + '_' + Date.now() + '.mp4';
const r2u = await uploadToR2(vb, filename, 'video/mp4'); saveClip(chatId, r2u || vid);
const form = new FormData();
form.append('chat_id', String(chatId));
form.append('video', new Blob([vb], { type: 'video/mp4' }), filename);
form.append('caption', '🌅 Seedance 16:9 — ' + (sceneKey || 'clip'));
await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/sendVideo', { method: 'POST', body: form });
await edit(chatId, msgId, '🌅 *Seedance 16:9*\n' + bar(10,10) + '\n✅ *Completado*');
} catch(e) { await edit(chatId, msgId, '✅ Video listo: ' + vid); }
} else { await edit(chatId, msgId, '❌ No generó. Verifica créditos OpenRouter.'); }
}
// ORACLE BACKGROUNDS — GPT Image 2 via Runway (async polling)
async function runOracleBackgrounds(chatId, onlyCity = null) {
const msgId = await send(chatId, '🌆 *Oracle Backgrounds*\n' + bar(0, 5) + '\n_Generando fondos..._');
const cities = [
{ name: 'berlin', prompt: 'Aerial view from extreme height above Berlin looking down, Fernsehturm TV tower visible, overcast sky golden hour light, photorealistic, cinematic, no people' },
{ name: 'tokyo', prompt: 'Aerial view from extreme height above Tokyo looking down, city grid to horizon, Tokyo Tower visible, dusk blue hour, neon lights, photorealistic, cinematic, no people' },
{ name: 'rio', prompt: 'Aerial view from extreme height above Rio de Janeiro looking down, Guanabara Bay, Christ the Redeemer visible below, tropical green hills, golden hour, photorealistic, cinematic, no people' },
{ name: 'dubai', prompt: 'Aerial view from Burj Khalifa height looking down, Dubai desert city below, glass towers, sunset orange sky, photorealistic, cinematic, no people' },
{ name: 'nyc', prompt: 'Aerial view from Empire State Building height looking down, Manhattan grid, Hudson and East River, morning golden light, photorealistic, cinematic, no people' },
];
const results = [];
console.log('RUNWAY_KEY prefix:', (process.env.RUNWAY_KEY || '').slice(0, 8));
const citiesToProcess = onlyCity ? cities.filter(c => c.name === onlyCity) : cities;
for (let i = 0; i < citiesToProcess.length; i++) {
const city = citiesToProcess[i];
await edit(chatId, msgId, '🌆 *Oracle Backgrounds*\n' + bar(i, 5) + '\n_📸 ' + city.name + '..._');
try {
const r = await fetch('https://api.dev.runwayml.com/v1/text_to_image', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({ model: 'gpt_image_2', promptText: city.prompt, ratio: '1920:1088' })
});
const d = await r.json();
if (!d.id) { await send(chatId, '❌ ' + city.name + ': ' + JSON.stringify(d).slice(0, 100)); continue; }
// Poll until complete
let imgUrl = null;
for (let j = 0; j < 30; j++) {
await new Promise(res => setTimeout(res, 5000));
const poll = await fetch('https://api.dev.runwayml.com/v1/tasks/' + d.id, {
headers: { 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' }
});
const t = await poll.json();
console.log(city.name + ' poll ' + j + ':', t.status);
if (t.status === 'SUCCEEDED') { imgUrl = t.output?.[0]; break; }
if (t.status === 'FAILED') break;
}
if (!imgUrl) { await send(chatId, '❌ ' + city.name + ': generación falló'); continue; }
const imgRes = await fetch(imgUrl);
const imgBuf = await imgRes.arrayBuffer();
const r2Url = await uploadToR2(imgBuf, 'oracle_bg_' + city.name + '.jpg', 'image/jpeg');
const finalUrl = r2Url || imgUrl;
results.push({ city: city.name, url: finalUrl });
await new Promise(res => setTimeout(res, 10000));
await photo(chatId, finalUrl, '🌆 ' + city.name + ' — ' + finalUrl);
} catch(e) { await send(chatId, '❌ ' + city.name + ': ' + e.message); }
}
await edit(chatId, msgId, '🌆 *Oracle Backgrounds*\n' + bar(5, 5) + '\n✅ *' + results.length + '/5 generados*');
if (results.length > 0) await send(chatId, '✅ *URLs R2:*\n' + results.map(r => r.city + ':\n' + r.url).join('\n\n'));
}
// ACCESS CONTROL — whitelist por chat ID. Override con env ALLOWED_CHAT_IDS="id1,id2,..."
const ALLOWED_CHAT_IDS = new Set(
(process.env.ALLOWED_CHAT_IDS || '1244921942')
.split(',').map(s => Number(s.trim())).filter(Boolean)
);
function logAccess(msg, authorized) {
const f = msg.from || {};
console.log('[ACCESS] ' + JSON.stringify({
ts: new Date().toISOString(),
authorized,
chat_id: msg.chat?.id,
user_id: f.id,
username: f.username || null,
first_name: f.first_name || null,
last_name: f.last_name || null,
text: (msg.text || msg.caption || (msg.photo ? '[photo]' : '')).slice(0, 200)
}));
}
// === REFRAME 16:9 con Runway Aleph (video-to-video) ===
async function reframeVideo(videoUrl, prompt) {
if (!process.env.RUNWAY_KEY) return { error: 'RUNWAY_KEY no configurada' };
try {
const r = await fetch('https://api.dev.runwayml.com/v1/video_to_video', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({
model: 'gen4_aleph',
videoUri: videoUrl,
promptText: prompt || 'Expand the scene to a wide 16:9 frame. Keep the original subject centered and unchanged. Fill the new left and right areas with coherent matching dark cinematic environment (ruins, smoke), solid and detailed, no transparent or ghostly elements.',
ratio: '1280:720'
})
});
const txt = await r.text();
console.log('Reframe:', txt.slice(0, 250));
let d; try { d = JSON.parse(txt); } catch(e) { return { error: 'resp no-JSON: ' + txt.slice(0,150) }; }
if (!d.id) return { error: txt.slice(0, 220) };
for (let i = 0; i < 60; i++) {
await new Promise(res => setTimeout(res, 10000));
const t = await (await fetch('https://api.dev.runwayml.com/v1/tasks/' + d.id, { headers: { 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' } })).json();
if (t.status === 'SUCCEEDED') return { url: t.output?.[0] };
if (t.status === 'FAILED') return { error: 'FAILED: ' + JSON.stringify(t.failure || t.failureCode || t).slice(0, 200) };
}
return { error: 'timeout (>10min)' };
} catch(e) { return { error: e.message }; }
}
async function runReframe(chatId, videoUrl) {
const msgId = await send(chatId, '🔄 *Reframe 16:9 (Runway Aleph)*\n' + bar(0,10) + '\n_Procesando... puede tardar varios min_');
const res = await reframeVideo(videoUrl, null);
if (res && res.url) {
await video(chatId, res.url, '🔄 Reframe 16:9 — Runway Aleph');
try {
const vb = await (await fetch(res.url)).arrayBuffer();
const r2u = await uploadToR2(vb, 'reframe_' + Date.now() + '.mp4', 'video/mp4');
if (r2u) saveClip(chatId, r2u);
} catch(e) {}
await edit(chatId, msgId, '🔄 *Reframe 16:9*\n' + bar(10,10) + '\n✅ *Completado*');
} else {
await edit(chatId, msgId, '❌ Reframe falló:\n`' + (res?.error || 'sin detalle') + '`');
}
}
// COMMAND HANDLER
async function handle(msg) {
const chatId = msg.chat.id;
const authorized = ALLOWED_CHAT_IDS.has(chatId);
logAccess(msg, authorized);
if (!authorized) return; // silent drop — no confirmamos existencia del bot
const text = msg.text || '';
if (msg.photo) {
const p = msg.photo[msg.photo.length - 1];
const caption = msg.caption || '';
const msgId = await send(chatId, '📸 Foto recibida — enviando a Runway...');
try {
const fr = await fetch('https://api.telegram.org/bot' + TELEGRAM_TOKEN + '/getFile?file_id=' + p.file_id);
const fd = await fr.json();
const imgUrl = 'https://api.telegram.org/file/bot' + TELEGRAM_TOKEN + '/' + fd.result?.file_path;
const ir = await fetch(imgUrl);
const ib = await ir.arrayBuffer();
const dataUrl = 'data:image/jpeg;base64,' + Buffer.from(ib).toString('base64');
const motionPrompt = caption || 'Slow 360 product rotation. Professional studio lighting. Dark background subtle neon reflection. Smooth rotation. Commercial product video.';
await edit(chatId, msgId, '🎬 Runway generando... (~2 min)');
const rr = await fetch('https://api.dev.runwayml.com/v1/image_to_video', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' },
body: JSON.stringify({ model: 'gen4_turbo', promptImage: dataUrl, promptText: motionPrompt, ratio: '720:1280', duration: 5 })
});
const rd = await rr.json();
if (!rd.id) { await edit(chatId, msgId, '❌ Runway: ' + JSON.stringify(rd).slice(0, 100)); return; }
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const t = await (await fetch('https://api.dev.runwayml.com/v1/tasks/' + rd.id, { headers: { 'Authorization': 'Bearer ' + process.env.RUNWAY_KEY, 'X-Runway-Version': '2024-11-06' } })).json();
if (t.status === 'SUCCEEDED') { await video(chatId, t.output?.[0], '🎬 Video listo'); await edit(chatId, msgId, '✅ Listo'); return; }
if (t.status === 'FAILED') { await edit(chatId, msgId, '❌ Runway falló.'); return; }
await edit(chatId, msgId, '🎬 Runway... ' + (i*10) + 's');
}
} catch(e) { await edit(chatId, msgId, '❌ Error: ' + e.message); }
return;
}
if (text === '/start') {
await send(chatId, '🎨 *maarmapa factory v7.5*\n\n*📝 Content*\n`/post [tema]` — Narrativa + 7 slides Grok + animados con Runway\n`/boykot [producto]` — Carrusel editorial negro/#CCFF00 para Boykot.cl\n\n*🖼 WebPost*\n`/webpost [tema]` — Busca en web, extrae imágenes, caption con DeepSeek\n`/webpost-lite [tema]` 🟢 — Sin fallback Grok, más rápido\n`/webpost-carousel [tema]` — Carrusel de slides con texto e imágenes\n`/webpost-carousel-lite [tema]` 🟢 — Carousel sin Grok fallback\n`/webpost-haiku-images [tema]` ⭐ — Ultra simple con Claude Haiku\n`/webpost-hyperframes [tema]` — Post con frames cinematográficos\n`/webpost-adobe [tema]` — Post via módulo Adobe MCP\n`/webpost-openrouter [tema]` — Post 100% vía OpenRouter\n\n*🎬 Video / Imágenes*\n`/runway [escena]` — Imagen Grok + animada con Runway gen4 (5s 9:16)\n`/seedance [escena]` — Video con Seedance vía OpenRouter (9:16 720p)\n`/seedance16 [escena]` 🆕 — Seedance en HORIZONTAL 16:9 widescreen\n`/reframe [URL]` 🔄 — Reencuadra un video existente a 16:9 (Runway Aleph)\n`/seedancemini [prompt]` 🆕 — Seedance Mini (rápido/barato)\n`/seedance4k [prompt]` 🆕 — Seedance en 4K\n`/seedream [prompt]` 🆕 — Imagen Seedream 5.0\n`/omniflash [prompt]` 🆕 — Google Omni Flash\n`/seedaudio [texto]` 🆕 — Seed Audio 1.0\n`/probemodelos` 🔬 — Lista los modelos disponibles (OpenRouter + Runway)\n`/squad` — 7 ángulos del squad Grok+Runway\n`/anime` — Character sheets + 3 shots animados con Runway\n\n*🌆 Oracle*\n`/oracle-bg` — Genera fondos de ciudades en altura para el Oracle (GPT Image 2 + R2)\n\n*🎵 Sync*\n`/syncr2` — Carga clips MP4 desde R2\n`/addclip [URL]` — Agrega clip manualmente\n`/clips` — Lista clips en memoria\n`/clearclips` — Borra lista de clips\n`/sync` — Mezcla clips con SOUTHSIDE BPM 103 → video final\n\n*💬 Utils*\n`/buscar [query]` — Búsqueda vía agente Grok\n`/chat [pregunta]` — Chat con DeepSeek\n`/digest` — Resumen semanal arte/blockchain/AI\n`/runs` 🆕 — Últimas corridas y su estado\n`/retry [id]` 🆕 — Retoma una corrida sin volver a pagar lo ya generado\n📸 *Foto* — Runway la convierte en video 5s');
return;
}
if (text.startsWith('/post ')) { runFactory(chatId, text.replace('/post ', '')).catch(e => send(chatId, '❌ ' + e.message)); return; }
if (text.startsWith('/boykot ')) { runBoykotPost(chatId, text.replace('/boykot ', '')).catch(e => send(chatId, '❌ ' + e.message)); return; }
if (text.startsWith('/anime') || text === '/anime') { runAnime(chatId, text.replace('/anime', '').trim() || 'southside').catch(e => send(chatId, '❌ ' + e.message)); return; }
if (text.startsWith("/oracle-bg")) { const bgCity = text.replace("/oracle-bg", "").trim() || null; runOracleBackgrounds(chatId, bgCity).catch(e => send(chatId, '❌ ' + e.message)); return; }
if (text === '/squad') { runSquad(chatId).catch(e => send(chatId, '❌ ' + e.message)); return; }
if (text.startsWith('/reframe')) {
const url = text.replace('/reframe', '').trim();
if (!url.startsWith('http')) { await send(chatId, '🔄 Uso: `/reframe [URL del video]`\nReencuadra de vertical a 16:9 rellenando los lados (Runway Aleph). Ojo: Aleph tiene límite de duración, probá con clips cortos.'); return; }
runReframe(chatId, url).catch(e => send(chatId, '❌ ' + e.message));
return;
}
if (text.startsWith('/seedancemini')) {
const p = text.replace('/seedancemini', '').trim();
if (!p) { await send(chatId, '🌱 Uso: `/seedancemini [prompt]` — video 9:16 720p con Seedance Mini'); return; }
runORModel(chatId, 'seedance mini', p, { kind: 'video' }).catch(e => send(chatId, '❌ ' + e.message));
return;
}
if (text.startsWith('/seedance4k')) {
const p = text.replace('/seedance4k', '').trim();
if (!p) { await send(chatId, '🌱 Uso: `/seedance4k [prompt]` — video Seedance en resolución 4K (más lento/caro)'); return; }
runORModel(chatId, 'seedance', p, { kind: 'video', resolution: '4k' }).catch(e => send(chatId, '❌ ' + e.message));
return;
}
if (text.startsWith('/seedream')) {
const p = text.replace('/seedream', '').trim();
if (!p) { await send(chatId, '🖼 Uso: `/seedream [prompt]` — imagen con Seedream 5.0'); return; }
runORModel(chatId, 'seedream 5', p, { kind: 'image' }).catch(e => send(chatId, '❌ ' + e.message));
return;
}
if (text.startsWith('/omniflash')) {
const p = text.replace('/omniflash', '').trim();
if (!p) { await send(chatId, '⚡ Uso: `/omniflash [prompt]` — Google Omni Flash (detecta solo si es video o imagen)'); return; }
runORModel(chatId, 'omni flash', p, {}).catch(e => send(chatId, '❌ ' + e.message));
return;
}