dikdimon commited on
Commit
32dee64
·
verified ·
1 Parent(s): 421d9d0

Delete sd-webui-timemachine-fixed

Browse files
sd-webui-timemachine-fixed/javascript/init.js DELETED
@@ -1,56 +0,0 @@
1
- (function(NAME) {
2
-
3
- const name = NAME.toLowerCase().replaceAll(/\s/g, '');
4
-
5
- let _r = 0;
6
- function to_gradio(v) {
7
- // force call `change` event on gradio
8
- return [v.toString(), (_r++).toString()];
9
- }
10
-
11
- function js2py(type, gradio_field, value) {
12
- // set `value` to gradio's field
13
- // (1) Click gradio's button.
14
- // (2) Gradio will fire js callback to retrieve value to be set.
15
- // (3) Gradio will fire another js callback to notify the process has been completed.
16
- return new Promise(resolve => {
17
- const callback_name = `${name}-${type}-${gradio_field}`;
18
-
19
- // (2)
20
- globalThis[callback_name] = () => {
21
-
22
- delete globalThis[callback_name];
23
-
24
- // (3)
25
- const callback_after = callback_name + '_after';
26
- globalThis[callback_after] = () => {
27
- delete globalThis[callback_after];
28
- resolve();
29
- };
30
-
31
- return to_gradio(value);
32
- };
33
-
34
- // (1)
35
- gradioApp().querySelector(`#${callback_name}_set`).click();
36
- });
37
- }
38
-
39
- function id(mode, s) {
40
- const v = `${name}-${mode}`;
41
- return s === undefined ? v : `${v}-${s}`;
42
- }
43
-
44
- if (!globalThis[name]) {
45
- globalThis[name] = {};
46
- }
47
-
48
- const obj = globalThis[name];
49
- obj.id = id;
50
- obj.js2py = js2py;
51
- obj.init = true;
52
-
53
- console.log(`[${NAME}] initialized`)
54
- document.dispatchEvent(new CustomEvent(`${name}_init`, { detail: obj }));
55
-
56
- })('TimeMachine');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/javascript/modules/chart.umd.js DELETED
The diff for this file is too large to render. See raw diff
 
sd-webui-timemachine-fixed/javascript/modules/chart.umd.js.map DELETED
The diff for this file is too large to render. See raw diff
 
sd-webui-timemachine-fixed/javascript/timemachine.js DELETED
@@ -1,957 +0,0 @@
1
- (function (NAME) {
2
-
3
- const name = NAME.toLowerCase().replaceAll(/\s/g, '');
4
-
5
- if (globalThis[name]?.init) init(name, globalThis[name]);
6
- else document.addEventListener(`${name}_init`, e => init(name, e.detail), { once: true });
7
-
8
- async function init(name, lib) { await load_modules(lib); await main(name, lib); }
9
-
10
- function load_modules(lib) {
11
- return new Promise(resolve => {
12
- function load() {
13
- if (lib.module_loaded) return true;
14
- const app = gradioApp();
15
- if (!app || app === document) return false;
16
- const jscont = app.querySelector('#' + lib.id('js_modules'));
17
- if (!jscont) return false;
18
- const [base_path, ...scripts] = jscont.textContent.trim().split('\n').map(x => x.trim());
19
- jscont.textContent = '';
20
- const df = document.createDocumentFragment();
21
- for (let src of scripts) {
22
- const s = document.createElement('script');
23
- s.async = true; s.type = 'module'; s.src = `file=${src}`; df.appendChild(s);
24
- }
25
- app.appendChild(df);
26
- lib.import = s => import(`/file=${base_path}/javascript/modules/${s}`);
27
- lib.module_loaded = true; resolve(); return true;
28
- }
29
- (function try_load() { if (!load()) setTimeout(try_load, 500); })();
30
- });
31
- }
32
-
33
- async function main(name, lib) {
34
- await lib.import('chart.umd.js');
35
- main2(name, lib, 'txt2img');
36
- main2(name, lib, 'img2img');
37
- }
38
-
39
- async function main2(name, lib, mode) {
40
- // Ждём появления аккордеона в DOM
41
- await new Promise(resolve => {
42
- (function try_get() {
43
- const el = gradioApp().querySelector('#' + lib.id(mode, 'accordion'));
44
- el ? resolve(el) : setTimeout(try_get, 500);
45
- })();
46
- });
47
-
48
- let initialized = false;
49
-
50
- function tryInit() {
51
- if (initialized) return;
52
- const container = gradioApp().querySelector('#' + lib.id(mode, 'container'));
53
- // offsetParent === null если элемент скрыт (display:none)
54
- if (container && container.offsetParent !== null) {
55
- initialized = true;
56
- main3(name, lib, mode);
57
- }
58
- }
59
-
60
- // Попытка сразу (Gradio 4.x: контент уже в DOM, просто скрыт)
61
- tryInit();
62
-
63
- // MutationObserver для Gradio 3.x (контент добавляется при открытии)
64
- const acc = gradioApp().querySelector('#' + lib.id(mode, 'accordion'));
65
- if (acc) {
66
- new MutationObserver(() => tryInit())
67
- .observe(acc, { childList: true, subtree: true, attributes: true });
68
- }
69
-
70
- // Опрос как запасной вариант (ловит CSS-изменения видимости)
71
- // Останавливается как только граф инициализирован или через 60 сек
72
- const deadline = Date.now() + 60000;
73
- const poll = setInterval(() => {
74
- if (initialized || Date.now() > deadline) { clearInterval(poll); return; }
75
- tryInit();
76
- }, 300);
77
- }
78
-
79
- function main3(name, lib, mode) {
80
- const id = s => lib.id(mode, s);
81
- const $ = x => Array.from(gradioApp().querySelectorAll(x)).at(-1);
82
- const $$ = x => gradioApp().querySelector(x);
83
-
84
- const enabled = $$(`#${id('enabled')} input[type=checkbox]`);
85
- const generate_button = $$(`#${mode}_generate`);
86
- const step_ele = $$(`#${mode}_steps input[type=number]`);
87
- const oneShotEl = $$(`#${id('one_shot')} input[type=checkbox]`);
88
- const cutoffEl = $$(`#${id('cutoff')} input[type=number]`);
89
-
90
- // ── Главный график ──────────────────────────────────
91
- const canvas = document.createElement('canvas');
92
- canvas.width = 512; canvas.height = 512;
93
-
94
- const plugins = createPlugins(canvas, step_ele, cutoffEl);
95
- const chart = new Chart(canvas.getContext('2d'), {
96
- type: 'scatter', data: createInitialData(),
97
- options: createChartOption(), plugins,
98
- });
99
- $('#' + id('container')).appendChild(canvas);
100
-
101
- // ── Per-segment mode: right-click on line → menu ─────────
102
- const SEG_MODES = ['Linear', 'Ease In', 'Ease Out', 'Ease In-Out', 'Cubic', 'Exponential', 'Step'];
103
-
104
- function _findSegment(c, ev) {
105
- const rect = c.canvas.getBoundingClientRect();
106
- const x = c.scales.x.getValueForPixel(ev.clientX - rect.left);
107
- const data = c.data.datasets[0].data;
108
- for (let i = 0; i < data.length - 1; i++) {
109
- if (data[i].x <= x && x <= data[i + 1].x) return { idx: i, pt: data[i] };
110
- }
111
- return null;
112
- }
113
-
114
- let _segMenuCloser = null;
115
- function _closeSegMenu() {
116
- const old = document.getElementById(id('segmenu')); if (old) old.remove();
117
- if (_segMenuCloser) { document.removeEventListener('click', _segMenuCloser); _segMenuCloser = null; }
118
- }
119
-
120
- function _showSegMenu(c, seg, ev) {
121
- _closeSegMenu();
122
- const div = document.createElement('div');
123
- div.id = id('segmenu');
124
- div.style.cssText = 'position:fixed;background:#1a1a2e;border:1px solid #ff8c00;border-radius:8px;padding:4px 0;z-index:99999;font-size:13px;box-shadow:0 4px 20px rgba(0,0,0,.6);min-width:150px';
125
- _segMenuCloser = () => _closeSegMenu();
126
- document.addEventListener('click', _segMenuCloser, {once:true});
127
-
128
- SEG_MODES.forEach(m => {
129
- const el = document.createElement('div');
130
- el.textContent = m;
131
- const active = seg.pt.mode === m || (!seg.pt.mode && m === 'Linear');
132
- el.style.cssText = `padding:4px 16px;cursor:pointer;color:${active?'#ff8c00':'#ddd'};font-weight:${active?'bold':'normal'}`;
133
- el.addEventListener('mouseenter', () => { el.style.background = '#ff8c0022'; });
134
- el.addEventListener('mouseleave', () => { el.style.background = 'transparent'; });
135
- el.addEventListener('click', e => { e.stopPropagation();
136
- if (m === 'Linear') delete seg.pt.mode; else seg.pt.mode = m;
137
- c.update(); saveState(c); triggerSync(c); _closeSegMenu();
138
- });
139
- div.appendChild(el);
140
- });
141
-
142
- const sep = document.createElement('div');
143
- sep.style.cssText = 'height:1px;background:#444;margin:3px 8px';
144
- div.appendChild(sep);
145
-
146
- const clear = document.createElement('div');
147
- clear.textContent = '— Use global';
148
- clear.style.cssText = 'padding:4px 16px;cursor:pointer;color:#999;font-style:italic';
149
- clear.addEventListener('mouseenter', () => { clear.style.background = '#ff8c0022'; });
150
- clear.addEventListener('mouseleave', () => { clear.style.background = 'transparent'; });
151
- clear.addEventListener('click', e => { e.stopPropagation();
152
- delete seg.pt.mode; c.update(); saveState(c); triggerSync(c); _closeSegMenu();
153
- });
154
- div.appendChild(clear);
155
-
156
- div.style.left = Math.min(ev.clientX, window.innerWidth - 170) + 'px';
157
- div.style.top = Math.min(ev.clientY, window.innerHeight - div.offsetHeight) + 'px';
158
- document.body.appendChild(div);
159
- }
160
-
161
- canvas.addEventListener('contextmenu', e => {
162
- e.preventDefault();
163
- if (plugins[0].lastRemoved) { plugins[0].lastRemoved = false; return; }
164
- const seg = _findSegment(chart, e);
165
- if (seg) _showSegMenu(chart, seg, e);
166
- }, true);
167
-
168
- // σ-аппроксимация (реальные через HTTP, fallback Karras)
169
- let showSigma = false, approxSigmas = [];
170
-
171
- function getScheduler() {
172
- const el = $$(`#${id('scheduler')} select`);
173
- return el?.value || 'Use sampler default';
174
- }
175
-
176
- function getSampler() {
177
- const el = $$(`#${mode}_sampling select`);
178
- return el?.value || '';
179
- }
180
- function getA1111Scheduler() {
181
- const el = $$(`#${mode}_scheduler select`);
182
- return el?.value || '';
183
- }
184
-
185
- async function _fetchRealSigmas(scheduler, steps) {
186
- try {
187
- let url = `/timemachine/sigmas?scheduler=${encodeURIComponent(scheduler)}&steps=${steps}`;
188
- const sampler = getSampler();
189
- if (sampler) url += `&sampler=${encodeURIComponent(sampler)}`;
190
- const a1111sched = getA1111Scheduler();
191
- if (a1111sched) url += `&a1111_scheduler=${encodeURIComponent(a1111sched)}`;
192
- const res = await fetch(url);
193
- if (!res.ok) return null;
194
- const data = await res.json();
195
- return Array.isArray(data.sigmas) ? data.sigmas : null;
196
- } catch(e) {
197
- return null;
198
- }
199
- }
200
-
201
- function _karrasApprox(n) {
202
- const [sMin, sMax, rho] = [0.1, 14.6, 7];
203
- const sig = Array.from({length: n}, (_, i) => {
204
- const t = i / Math.max(1, n-1);
205
- return Math.pow(Math.pow(sMax,1/rho)+t*(Math.pow(sMin,1/rho)-Math.pow(sMax,1/rho)), rho);
206
- });
207
- sig.push(0);
208
- return sig;
209
- }
210
-
211
- function updateSigmaApprox(n, scheduler) {
212
- approxSigmas = _karrasApprox(n);
213
- _fetchRealSigmas(scheduler, n).then(real => {
214
- if (real && real.length >= n) {
215
- // real has n+1 sigmas (including trailing 0)
216
- approxSigmas = real.slice(0, n + 1);
217
- if (showSigma) { chart?.update(); if (hr_chart) hr_chart?.update(); }
218
- }
219
- });
220
- }
221
-
222
- // Динамические callbacks Chart.js
223
- chart.options.scales.y.ticks.callback = v => {
224
- if (!showSigma || v < 1 || v > approxSigmas.length-1) return v;
225
- const s = approxSigmas[Math.round(v)-1];
226
- return s == null ? v : s < 0.01 ? '0' : s < 1 ? s.toFixed(3) : s.toFixed(1);
227
- };
228
- chart.options.plugins.tooltip.callbacks.title = ctxs => ctxs.map(c => {
229
- const pos = c.parsed.y;
230
- if (!showSigma) return `${c.parsed.x} → ${pos}`;
231
- const s = approxSigmas[Math.round(pos)-1];
232
- return `step ${c.parsed.x} → ${pos}${s != null ? ` (σ≈${s.toFixed(3)})` : ''}`;
233
- });
234
-
235
- // Interp tension
236
- function getInterpMode() {
237
- return $$(`#${id('interpolation')} select`)?.value || 'Linear';
238
- }
239
- function updateChartStyle(c = chart) {
240
- const mode = getInterpMode();
241
- const ds = c.data.datasets[0];
242
- if (mode === 'Step') {
243
- ds.stepped = 'before'; ds.cubicInterpolationMode = 'default'; ds.tension = 0;
244
- } else if (mode === 'Monotone') {
245
- ds.stepped = false; ds.cubicInterpolationMode = 'monotone'; ds.tension = 0;
246
- } else if (mode === 'Smooth (spline)') {
247
- ds.stepped = false; ds.cubicInterpolationMode = 'default'; ds.tension = 0.4;
248
- } else {
249
- ds.stepped = false; ds.cubicInterpolationMode = 'default'; ds.tension = 0;
250
- }
251
- c.update();
252
- }
253
- const interpEl = $$(`#${id('interpolation')} select`);
254
- if (interpEl) interpEl.addEventListener('change', () => updateChartStyle());
255
- const schedEl = $$(`#${id('scheduler')} select`);
256
- if (schedEl) schedEl.addEventListener('change', () => {
257
- updateSigmaApprox(+step_ele.value, getScheduler());
258
- });
259
- const samplerEl = $$(`#${mode}_sampler select`);
260
- if (samplerEl) samplerEl.addEventListener('change', () => {
261
- updateSigmaApprox(+step_ele.value, getScheduler());
262
- });
263
-
264
- // ── История (Undo/Redo) ──────────────────────────────
265
- const MAX_HIST = 20;
266
- let hist = [], histIdx = -1;
267
-
268
- function saveState(c) {
269
- const snap = JSON.stringify(c.data.datasets[0].data);
270
- if (hist[histIdx] === snap) return;
271
- hist = hist.slice(0, histIdx+1);
272
- hist.push(snap);
273
- if (hist.length > MAX_HIST) hist.shift(); else histIdx++;
274
- }
275
- function undo(c) {
276
- if (histIdx > 0) {
277
- histIdx--;
278
- c.data.datasets[0].data = JSON.parse(hist[histIdx]);
279
- c.update(); triggerSync(c);
280
- }
281
- }
282
- function redo(c) {
283
- if (histIdx < hist.length-1) {
284
- histIdx++;
285
- c.data.datasets[0].data = JSON.parse(hist[histIdx]);
286
- c.update(); triggerSync(c);
287
- }
288
- }
289
-
290
- document.addEventListener('keydown', e => {
291
- const tag = document.activeElement?.tagName;
292
- if (tag==='INPUT'||tag==='TEXTAREA'||document.activeElement?.contentEditable==='true') return;
293
- if (!enabled?.checked) return;
294
- if (e.ctrlKey && e.key==='z' && !e.shiftKey) { e.preventDefault(); undo(chart); }
295
- if (e.ctrlKey && ((e.key==='z' && e.shiftKey)||e.key==='y')) { e.preventDefault(); redo(chart); }
296
- });
297
-
298
- // ── Sync + Persist ──────────────────────────────────
299
- let syncedData = null, pendingSync = null;
300
-
301
- function _saveToDisk() {
302
- try {
303
- localStorage.setItem(`tm_curve:${mode}`, JSON.stringify(chart.data.datasets[0].data));
304
- if (hr_chart) {
305
- localStorage.setItem(`tm_curve:${mode}:hr`, JSON.stringify(hr_chart.data.datasets[0].data));
306
- }
307
- } catch(e) { /* quota exceeded, silently ignore */ }
308
- }
309
-
310
- function _loadFromDisk() {
311
- const saved = localStorage.getItem(`tm_curve:${mode}`);
312
- if (saved) {
313
- try {
314
- const pts = JSON.parse(saved);
315
- if (Array.isArray(pts) && pts.length >= 2) {
316
- chart.data.datasets[0].data = pts;
317
- updateSteps(chart, +step_ele.value);
318
- syncedData = JSON.stringify(chart.data.datasets[0].data);
319
- return;
320
- }
321
- } catch(e) { /* ignore corrupt data */ }
322
- }
323
- }
324
-
325
- function triggerSync(c, key='tm') {
326
- const data = JSON.stringify(c.data.datasets[0].data);
327
- if (key==='tm' && data===syncedData) return;
328
- if (key==='tm_hr' && data===hr_syncedData) return;
329
- const prom = lib.js2py(mode, key, data).then(() => {
330
- if (key==='tm') { syncedData = data; pendingSync = null; }
331
- if (key==='tm_hr') { hr_syncedData = data; hr_pendingSync = null; }
332
- }).catch(() => {
333
- if (key==='tm') pendingSync = null;
334
- if (key==='tm_hr') hr_pendingSync = null;
335
- });
336
- if (key==='tm') pendingSync = prom;
337
- if (key==='tm_hr') hr_pendingSync = prom;
338
- _saveToDisk();
339
- }
340
-
341
- // Hook плагина
342
- const plugin = plugins[0];
343
- const _oEnd = plugin.endDrag.bind(plugin);
344
- plugin.endDrag = (c,a)=>{ _oEnd(c,a); saveState(c); triggerSync(c); };
345
- const _oAdd = plugin.addPoint.bind(plugin);
346
- plugin.addPoint = (c,a)=>{ _oAdd(c,a); saveState(c); triggerSync(c); };
347
- const _oDel = plugin.removePoint.bind(plugin);
348
- plugin.removePoint = (c,a)=>{ _oDel(c,a); saveState(c); triggerSync(c); };
349
-
350
- // Инициализация
351
- updateSteps(chart, +step_ele.value);
352
- updateSigmaApprox(+step_ele.value, getScheduler());
353
- _loadFromDisk();
354
- saveState(chart);
355
-
356
- let debounceTimer;
357
- step_ele.addEventListener('input', () => {
358
- updateSteps(chart, +step_ele.value);
359
- updateSigmaApprox(+step_ele.value, getScheduler());
360
- saveState(chart);
361
- if (cutoffEl) cutoffEl.setAttribute('max', +step_ele.value);
362
- clearTimeout(debounceTimer);
363
- debounceTimer = setTimeout(() => triggerSync(chart), 150);
364
- });
365
-
366
- // Обновление линии cutoff при изменении значения
367
- if (cutoffEl) {
368
- cutoffEl.addEventListener('input', () => {
369
- chart.update();
370
- if (hr_chart) hr_chart.update();
371
- });
372
- }
373
-
374
- // ── HR Fix график ────────────────────────────────────
375
- let hr_chart = null, hr_syncedData = null, hr_pendingSync = null;
376
- const hr_container = $$('#' + id('hr_container'));
377
-
378
- if (hr_container) {
379
- const hr_step_raw = $$(`#${mode}_hires_steps input[type=number]`);
380
- const hr_step = hr_step_raw || step_ele;
381
-
382
- const hr_canvas = document.createElement('canvas');
383
- hr_canvas.width = 512; hr_canvas.height = 512;
384
- const hr_plugins = createPlugins(hr_canvas, hr_step, cutoffEl);
385
-
386
- hr_chart = new Chart(hr_canvas.getContext('2d'), {
387
- type: 'scatter', data: createInitialData(),
388
- options: createChartOption(), plugins: hr_plugins,
389
- });
390
-
391
- hr_chart.options.scales.y.ticks.callback = v => {
392
- if (!showSigma || v < 1 || v > approxSigmas.length-1) return v;
393
- const s = approxSigmas[Math.round(v)-1];
394
- return s == null ? v : s < 1 ? s.toFixed(3) : s.toFixed(1);
395
- };
396
- hr_chart.options.plugins.tooltip.callbacks.title = ctxs => ctxs.map(c => {
397
- const pos = c.parsed.y;
398
- if (!showSigma) return `HR step ${c.parsed.x} → ${pos}`;
399
- const s = approxSigmas[Math.round(pos)-1];
400
- return `HR step ${c.parsed.x} → ${pos}${s != null ? ` (σ≈${s.toFixed(3)})` : ''}`;
401
- });
402
-
403
- hr_container.appendChild(hr_canvas);
404
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
405
-
406
- // Load persisted HR curve
407
- const savedHr = localStorage.getItem(`tm_curve:${mode}:hr`);
408
- if (savedHr) {
409
- try {
410
- const pts = JSON.parse(savedHr);
411
- if (Array.isArray(pts) && pts.length >= 2) {
412
- hr_chart.data.datasets[0].data = pts;
413
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
414
- hr_syncedData = JSON.stringify(hr_chart.data.datasets[0].data);
415
- }
416
- } catch(e) { /* ignore */ }
417
- }
418
-
419
- const hr_plugin = hr_plugins[0];
420
- const _hoEnd = hr_plugin.endDrag.bind(hr_plugin);
421
- hr_plugin.endDrag = (c,a)=>{ _hoEnd(c,a); triggerSync(c,'tm_hr'); };
422
- const _hoAdd = hr_plugin.addPoint.bind(hr_plugin);
423
- hr_plugin.addPoint = (c,a)=>{ _hoAdd(c,a); triggerSync(c,'tm_hr'); };
424
- const _hoDel = hr_plugin.removePoint.bind(hr_plugin);
425
- hr_plugin.removePoint = (c,a)=>{ _hoDel(c,a); triggerSync(c,'tm_hr'); };
426
-
427
- hr_step.addEventListener('input', () => {
428
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
429
- triggerSync(hr_chart, 'tm_hr');
430
- });
431
-
432
- // Синхронизируем tension HR графика с основным
433
- if (interpEl) interpEl.addEventListener('change', () => updateChartStyle(hr_chart));
434
- }
435
-
436
- // ── Generate button ─────────────────────────────────
437
- let generating = false;
438
- gradioApp().addEventListener('click', async e => {
439
- if (e.target !== generate_button) return;
440
- if (!enabled?.checked) return;
441
- if (generating) { generating = false; return; }
442
-
443
- const mainData = JSON.stringify(chart.data.datasets[0].data);
444
- const hrData = hr_chart ? JSON.stringify(hr_chart.data.datasets[0].data) : null;
445
- const needMain = mainData !== syncedData;
446
- const needHR = hrData !== null && hrData !== hr_syncedData;
447
-
448
- if (needMain || needHR) {
449
- e.preventDefault(); e.stopPropagation();
450
- await Promise.all([
451
- needMain ? (pendingSync || lib.js2py(mode,'tm', mainData).then(()=>syncedData =mainData)) : Promise.resolve(),
452
- needHR ? (hr_pendingSync || lib.js2py(mode,'tm_hr',hrData ).then(()=>hr_syncedData=hrData )) : Promise.resolve(),
453
- ]);
454
- generating = true; generate_button.click();
455
- }
456
- }, true);
457
-
458
- // ── Нижняя панель ───────────────────────────────────
459
- const PRESETS = {
460
- 'Linear (Default)': n => [{x:1,y:1},{x:n,y:n}],
461
- 'Detail Enhancer': n => [{x:1,y:1},{x:Math.max(2,Math.round(n*.25)),y:Math.round(n*.7)},{x:n,y:n}],
462
- 'Composition Lock': n => [{x:1,y:1},{x:Math.round(n*.75),y:Math.max(2,Math.round(n*.25))},{x:n,y:n}],
463
- 'Time Travel': n => [{x:1,y:1},{x:Math.round(n*.35),y:Math.round(n*.35)},{x:Math.round(n*.5),y:Math.max(1,Math.round(n*.15))},{x:Math.round(n*.65),y:Math.round(n*.35)},{x:n,y:n}],
464
- 'Early Burst': n => [{x:1,y:1},{x:Math.round(n*.5),y:Math.round(n*.9)},{x:n,y:n}],
465
- };
466
-
467
- function makeBtn(label, title, onClick) {
468
- const b = document.createElement('button');
469
- b.textContent = label; b.type = 'button'; b.title = title || label;
470
- b.style.cssText = 'padding:3px 10px;border-radius:4px;cursor:pointer;font-size:12px';
471
- b.addEventListener('click', onClick); return b;
472
- }
473
-
474
- const bar = document.createElement('div');
475
- bar.style.cssText = 'display:flex;gap:6px;align-items:center;margin:6px 0 0;flex-wrap:wrap;font-size:13px';
476
-
477
- // ── Пресеты (built-in + custom из localStorage) ─────
478
- function _storageKeys(prefix) {
479
- const keys = [];
480
- for (let i = 0; i < localStorage.length; i++) {
481
- const k = localStorage.key(i);
482
- if (k && k.startsWith(prefix)) keys.push(k);
483
- }
484
- return keys;
485
- }
486
-
487
- function _loadCustomPresets() {
488
- const out = {};
489
- for (const k of _storageKeys('tm_presets:')) {
490
- const name = k.slice('tm_presets:'.length);
491
- try {
492
- const raw = JSON.parse(localStorage.getItem(k));
493
- let pts, savedN;
494
- if (Array.isArray(raw)) {
495
- pts = raw;
496
- savedN = Math.max(...pts.map(p => p.x));
497
- } else if (raw && Array.isArray(raw.points) && raw.points.length >= 2) {
498
- pts = raw.points;
499
- savedN = raw.savedN || Math.max(...pts.map(p => p.x));
500
- } else {
501
- continue;
502
- }
503
- if (pts.length >= 2) {
504
- out[name] = (n) => {
505
- if (n === savedN || savedN <= 1) return pts;
506
- const scale = (n - 1) / (savedN - 1);
507
- return pts.map(p => ({
508
- ...p,
509
- x: Math.max(1, Math.min(n, Math.round(1 + (p.x - 1) * scale))),
510
- }));
511
- };
512
- }
513
- } catch(e) { /* ignore */ }
514
- }
515
- return out;
516
- }
517
-
518
- function _rebuildPresetDropdown() {
519
- while (presetSel.firstChild) presetSel.removeChild(presetSel.firstChild);
520
- // Built-in
521
- Object.keys(PRESETS).forEach(n => {
522
- const o = document.createElement('option'); o.value = n; o.textContent = n;
523
- presetSel.appendChild(o);
524
- });
525
- // Custom
526
- const custom = _loadCustomPresets();
527
- const cKeys = Object.keys(custom);
528
- if (cKeys.length) {
529
- const grp = document.createElement('optgroup'); grp.label = '-- Saved --';
530
- cKeys.forEach(n => {
531
- const o = document.createElement('option'); o.value = n; o.textContent = n;
532
- grp.appendChild(o);
533
- });
534
- presetSel.appendChild(grp);
535
- }
536
- // Store custom map on the select for the apply handler
537
- presetSel._custom = custom;
538
- }
539
-
540
- const presetSel = document.createElement('select');
541
- presetSel.style.cssText = 'padding:3px 6px;border-radius:4px;cursor:pointer;flex:1;min-width:120px';
542
- _rebuildPresetDropdown();
543
-
544
- const applyBtn = makeBtn('Apply', 'Применить пресет', () => {
545
- const name = presetSel.value;
546
- const fn = PRESETS[name] || presetSel._custom[name];
547
- if (!fn) return;
548
- const n = Math.max(2, +step_ele.value);
549
- chart.data.datasets[0].data = fn(n);
550
- updateSteps(chart, n);
551
- saveState(chart); updateChartStyle(); triggerSync(chart);
552
- });
553
-
554
- const savePresetBtn = makeBtn('Save', 'Сохранить как пресет', () => {
555
- const name = prompt('Название пресета:', '');
556
- if (!name) return;
557
- try {
558
- const pts = chart.data.datasets[0].data.map(p => ({...p}));
559
- localStorage.setItem(`tm_presets:${name}`, JSON.stringify({
560
- savedN: Math.max(2, +step_ele.value),
561
- points: pts,
562
- }));
563
- } catch(e) { alert('Не удалось сохранить пресет (превышен лимит localStorage).'); return; }
564
- _rebuildPresetDropdown();
565
- presetSel.value = name;
566
- });
567
-
568
- const delPresetBtn = makeBtn('Del', 'Удалить выбранный пресет', () => {
569
- const name = presetSel.value;
570
- if (!presetSel._custom[name]) return;
571
- if (!confirm(`Удалить пресет "${name}"?`)) return;
572
- localStorage.removeItem(`tm_presets:${name}`);
573
- _rebuildPresetDropdown();
574
- });
575
-
576
- const resetBtn = makeBtn('Reset', 'Сбросить кривую к прямой', () => {
577
- const n = Math.max(2, +step_ele.value);
578
- chart.data.datasets[0].data = [{x:1,y:1},{x:n,y:n}];
579
- saveState(chart); updateChartStyle(); triggerSync(chart);
580
- });
581
-
582
- // Undo / Redo
583
- const undoBtn = makeBtn('Undo', 'Отменить (Ctrl+Z)', () => undo(chart));
584
- const redoBtn = makeBtn('Redo', 'Повторить (Ctrl+Shift+Z)', () => redo(chart));
585
-
586
- // Separator
587
- const sep = () => { const s = document.createElement('span'); s.textContent='|'; s.style.opacity='.3'; return s; };
588
-
589
- // Export
590
- const exportBtn = makeBtn('Export', 'Сохранить кривую в JSON', () => {
591
- const n = prompt('Название пресета:', 'my_curve') || 'my_curve';
592
- const data = {
593
- name: n, version: 1,
594
- points: chart.data.datasets[0].data,
595
- interpolation: getInterpMode(),
596
- };
597
- const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
598
- const url = URL.createObjectURL(blob);
599
- const a = document.createElement('a');
600
- a.href = url; a.download = `${n.replace(/\s+/g,'_')}.json`; a.click();
601
- URL.revokeObjectURL(url);
602
- });
603
-
604
- // Import
605
- const importBtn = makeBtn('Import', 'Загрузить кривую из JSON', () => {
606
- const inp = document.createElement('input');
607
- inp.type = 'file'; inp.accept = '.json';
608
- inp.addEventListener('change', () => {
609
- const file = inp.files[0]; if (!file) return;
610
- const reader = new FileReader();
611
- reader.onload = ev => {
612
- try {
613
- const d = JSON.parse(ev.target.result);
614
- if (!Array.isArray(d.points)) return;
615
- const n = Math.max(2, +step_ele.value);
616
- const pts = d.points.filter(p => p.x>=1&&p.x<=n&&p.y>=1&&p.y<=n);
617
- chart.data.datasets[0].data = pts;
618
- updateSteps(chart, n);
619
- saveState(chart); triggerSync(chart);
620
- } catch(err) { console.error('[TimeMachine] Import error:', err); }
621
- };
622
- reader.readAsText(file);
623
- });
624
- inp.click();
625
- });
626
-
627
- // Show sigma
628
- const sigmaBtn = makeBtn('Sigma', 'Показать σ-значения на оси Y (реальные через HTTP, fallback Karras)', () => {
629
- showSigma = !showSigma;
630
- sigmaBtn.textContent = showSigma ? 'Step' : 'Sigma';
631
- chart.update(); if (hr_chart) hr_chart.update();
632
- });
633
-
634
- // ── Panel system ───────────────────────────────────────���────────
635
- let _panelDiv = null;
636
- function _closePanel() { if (_panelDiv) { _panelDiv.remove(); _panelDiv = null; } }
637
-
638
- function _openPanel(title, buildFn) {
639
- _closePanel();
640
- const d = document.createElement('div');
641
- d.id = id('panel');
642
- d.style.cssText = 'padding:8px;margin:4px 0;border:1px solid rgba(255,140,0,0.3);border-radius:6px;background:rgba(255,140,0,0.05);font-size:13px';
643
- const h = document.createElement('div');
644
- h.style.cssText = 'font-weight:bold;margin-bottom:6px';
645
- h.textContent = title;
646
- d.appendChild(h);
647
- buildFn(d);
648
- $('#' + id('container')).appendChild(d);
649
- _panelDiv = d;
650
- }
651
-
652
- // ── Gen… (procedural curves) ────────────────────────────────────
653
- const genBtn = makeBtn('Gen…', 'Сгенерировать кривую (синус / случайная)', () => {
654
- let prevData = chart.data.datasets[0].data.slice();
655
- function getN() { return Math.max(2, +step_ele.value); }
656
-
657
- function _sine(freq, amp) {
658
- const n = getN();
659
- const pts = [{x:1,y:1}];
660
- for (let x = 2; x < n; x++) {
661
- const t = (x-1)/(n-1);
662
- const mid = 1 + (n-1)*t;
663
- const y = Math.round(mid + amp*(n-1)*Math.sin(2*Math.PI*freq*t));
664
- pts.push({x, y: Math.max(1, Math.min(n, y))});
665
- }
666
- pts.push({x:n, y:n});
667
- return pts;
668
- }
669
-
670
- function _walk(stepSz, seed) {
671
- const n = getN();
672
- let rng = seed || Date.now();
673
- function rand() { rng ^= rng<<13; rng ^= rng>>17; rng ^= rng<<5; return (rng>>>0)/4294967296; }
674
- const pts = [{x:1,y:1}];
675
- let y = 1;
676
- for (let x = 2; x < n; x++) {
677
- y += (rand()-0.5)*2*stepSz;
678
- y = Math.max(1, Math.min(n, Math.round(y)));
679
- pts.push({x, y});
680
- }
681
- pts.push({x:n, y:n});
682
- return pts;
683
- }
684
-
685
- _openPanel('Generate Curve', panel => {
686
- const row1 = document.createElement('div');
687
- row1.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px';
688
- const sel = document.createElement('select');
689
- sel.innerHTML = '<option>Sine</option><option>Random walk</option>';
690
-
691
- const p1Label = document.createElement('span');
692
- p1Label.style.cssText = 'width:55px;text-align:right';
693
- const p1 = document.createElement('input');
694
- p1.type = 'range'; p1.min = 0.5; p1.max = 10; p1.step = 0.5; p1.value = 2;
695
-
696
- const p2Label = document.createElement('span');
697
- p2Label.style.cssText = 'width:55px;text-align:right';
698
- const p2 = document.createElement('input');
699
- p2.type = 'range'; p2.min = 0; p2.max = 1; p2.step = 0.05; p2.value = 0.3;
700
-
701
- function updateLabels() {
702
- if (sel.value === 'Sine') {
703
- p1Label.textContent = `Freq: ${p1.value}`;
704
- p1.min = 0.5; p1.max = 10; p1.step = 0.5;
705
- p2Label.textContent = `Amp: ${p2.value}`;
706
- p2.min = 0; p2.max = 1; p2.step = 0.05;
707
- } else {
708
- p1Label.textContent = `Step: ${p1.value}`;
709
- p1.min = 0.1; p1.max = 5; p1.step = 0.1;
710
- p2Label.textContent = `Seed: ${p2.value}`;
711
- p2.min = 0; p2.max = 9999; p2.step = 1;
712
- }
713
- }
714
-
715
- function preview() {
716
- const n = getN();
717
- const pts = sel.value === 'Sine' ? _sine(+p1.value, +p2.value) : _walk(+p1.value, +p2.value);
718
- chart.data.datasets[0].data = pts;
719
- updateSteps(chart, n);
720
- chart.update();
721
- }
722
-
723
- sel.addEventListener('change', () => { updateLabels(); preview(); });
724
- p1.addEventListener('input', () => { updateLabels(); preview(); });
725
- p2.addEventListener('input', () => { updateLabels(); preview(); });
726
- updateLabels(); preview();
727
-
728
- const apply = makeBtn('Apply', '', () => {
729
- saveState(chart); updateChartStyle(); triggerSync(chart);
730
- _closePanel();
731
- });
732
- const cancel = makeBtn('Cancel', '', () => {
733
- const n = getN();
734
- chart.data.datasets[0].data = prevData;
735
- updateSteps(chart, n); chart.update();
736
- _closePanel();
737
- });
738
- row1.append(sel, p1Label, p1, p2Label, p2, apply, cancel);
739
- panel.appendChild(row1);
740
- });
741
- });
742
-
743
- // ── Rasterize control points → dense array of length n ───────
744
- const _WEIGHT = {
745
- 'Ease In': t => t * t,
746
- 'Ease Out': t => 1 - (1-t)*(1-t),
747
- 'Ease In-Out': t => t*t*(3-2*t),
748
- 'Cubic': t => t*t*t,
749
- 'Exponential': t => Math.pow(2,t)-1,
750
- 'Step': t => 0,
751
- };
752
-
753
- function _rasterize(pts, n) {
754
- const sorted = [...pts].sort((a,b)=>a.x-b.x);
755
- const out = [];
756
- let si = 0;
757
- for (let x = 1; x <= n; x++) {
758
- while (si < sorted.length-2 && sorted[si+1].x <= x) si++;
759
- const p0 = sorted[si], p1 = sorted[Math.min(si+1, sorted.length-1)];
760
- let y;
761
- if (p0.x === p1.x) { y = p0.y; }
762
- else {
763
- const mode = p0.mode || '';
764
- const t = (x-p0.x)/(p1.x-p0.x);
765
- if (mode === 'Step') { y = p0.y; }
766
- else {
767
- const w = _WEIGHT[mode] || (t => t);
768
- y = p0.y + w(t) * (p1.y - p0.y);
769
- }
770
- }
771
- out.push({x, y: Math.max(1, Math.min(n, Math.round(y)))});
772
- }
773
- return out;
774
- }
775
-
776
- // ── Blend… ──────────────────────────────────────────────────────
777
- const blendBtn = makeBtn('Blend…', 'Смешать два пресета', () => {
778
- const n = Math.max(2, +step_ele.value);
779
- let prevData = chart.data.datasets[0].data.slice();
780
- const allPresets = {...PRESETS, ...presetSel._custom};
781
- const names = Object.keys(allPresets);
782
-
783
- _openPanel('Blend Presets', panel => {
784
- const row = document.createElement('div');
785
- row.style.cssText = 'display:flex;gap:6px;align-items:center;flex-wrap:wrap';
786
-
787
- const sA = document.createElement('select');
788
- const sB = document.createElement('select');
789
- names.forEach(name => { sA.innerHTML += `<option>${name}</option>`; sB.innerHTML += `<option>${name}</option>`; });
790
- if (names.length > 1) sB.selectedIndex = 1;
791
-
792
- const sl = document.createElement('input');
793
- sl.type = 'range'; sl.min = 0; sl.max = 100; sl.value = 50;
794
- const slLabel = document.createElement('span');
795
- slLabel.style.cssText = 'width:40px';
796
-
797
- function _getPresetFn(name) { return PRESETS[name] || presetSel._custom[name]; }
798
-
799
- function _blend() {
800
- const fnA = _getPresetFn(sA.value);
801
- const fnB = _getPresetFn(sB.value);
802
- if (!fnA || !fnB) return null;
803
- const cA = fnA(n), cB = fnB(n);
804
- const denseA = _rasterize(cA, n);
805
- const denseB = _rasterize(cB, n);
806
- const t = +sl.value / 100;
807
- slLabel.textContent = `${sl.value}%`;
808
- return denseA.map((pt, i) => ({
809
- x: pt.x,
810
- y: Math.round(pt.y*(1-t) + denseB[i].y*t),
811
- }));
812
- }
813
-
814
- function preview() { const pts = _blend(); if (pts) { chart.data.datasets[0].data = pts; updateSteps(chart, n); chart.update(); } }
815
-
816
- sA.addEventListener('change', preview);
817
- sB.addEventListener('change', preview);
818
- sl.addEventListener('input', preview);
819
- preview();
820
-
821
- const apply = makeBtn('Apply', '', () => { saveState(chart); updateChartStyle(); triggerSync(chart); _closePanel(); });
822
- const cancel = makeBtn('Cancel', '', () => { chart.data.datasets[0].data = prevData; updateSteps(chart, n); chart.update(); _closePanel(); });
823
-
824
- row.append(sA, sB, sl, slLabel, apply, cancel);
825
- panel.appendChild(row);
826
- });
827
- });
828
-
829
- bar.append(presetSel, applyBtn, savePresetBtn, delPresetBtn, resetBtn, sep(), undoBtn, redoBtn, sep(), exportBtn, importBtn, sep(), sigmaBtn, genBtn, blendBtn);
830
- $('#' + id('container')).appendChild(bar);
831
-
832
- // ── One-shot: автосброс Enabled после генерации ────────────────
833
- // Паттерн из script.js A1111: следим за interruptBtn.style.display
834
- if (oneShotEl) {
835
- const interruptBtn = gradioApp().querySelector(`#${mode}_interrupt`);
836
- if (interruptBtn) {
837
- let wasGen = false;
838
- new MutationObserver(() => {
839
- const isGen = interruptBtn.style.display === 'block';
840
- if (isGen && !wasGen) {
841
- wasGen = true;
842
- } else if (!isGen && wasGen) {
843
- wasGen = false;
844
- if (oneShotEl.checked && enabled?.checked) {
845
- enabled.checked = false;
846
- enabled.dispatchEvent(new Event('change', { bubbles: true }));
847
- }
848
- }
849
- }).observe(interruptBtn, { attributes: true, attributeFilter: ['style'] });
850
- }
851
- }
852
- }
853
-
854
- // ── Общие функции ────────────────────────────────────────────────
855
-
856
- function createInitialData() {
857
- return { datasets: [{
858
- type:'line', showLine:true, tension:0,
859
- backgroundColor:'rgba(255,140,0,.6)', borderColor:'rgba(255,140,0,.6)',
860
- borderWidth:2, borderCapStyle:'round', borderJoinStyle:'round',
861
- borderDash:[], borderDashOffset:0,
862
- pointBorderColor:'rgba(255,140,0,.6)', pointBackgroundColor:'rgba(255,140,0,.6)',
863
- pointBorderWidth:1, pointHoverBorderWidth:10, pointRadius:5, pointHitRadius:10,
864
- fill:false, data:[],
865
- }]};
866
- }
867
-
868
- function createChartOption() {
869
- return {
870
- responsive:false,
871
- events:['mouseup','mousedown','mousemove','mouseout','click','touchstart','touchmove'],
872
- scales:{
873
- x:{type:'linear',display:true,title:{display:true,text:'timesteps'},ticks:{major:{enabled:true}},min:0,max:100,stepSize:10},
874
- y:{type:'linear',display:true,title:{display:true,text:'actual timesteps'},ticks:{major:{enabled:true}},min:0,max:100,stepSize:10},
875
- },
876
- chartArea:{backgroundColor:'rgba(255,255,255,1)'},
877
- animation:{duration:100},
878
- plugins:{legend:{display:false},tooltip:{callbacks:{
879
- title:ctxs=>ctxs.map(c=>`${c.parsed.x} → ${c.parsed.y}`), label:()=>'',
880
- }}},
881
- };
882
- }
883
-
884
- function createPlugins(canvas, step_ele, cutoffEl) {
885
- const plugins = [{
886
- id:'dragpoint',
887
- beforeEvent(chart, args) {
888
- const t = args.event.type;
889
- if (t === 'mousedown') {
890
- const btn = args.event.native.button;
891
- if (btn===0) this.startDrag(chart, args);
892
- else if (btn===2) this.removePoint(chart, args);
893
- } else if (t==='mouseup'||t==='mouseout') {
894
- if (this.dragctx.item) this.endDrag(chart, args);
895
- } else if (t==='mousemove') {
896
- if (this.dragctx.item) this.onDrag(chart, args);
897
- }
898
- },
899
- afterDraw(chart) {
900
- const ctx = chart.ctx; ctx.save();
901
- // Cutoff line
902
- const cs = cutoffEl ? +cutoffEl.value : 0;
903
- if (cs > 0) {
904
- const xa = chart.scales.x, ya = chart.scales.y, x = xa.getPixelForValue(cs);
905
- ctx.beginPath(); ctx.setLineDash([6, 4]); ctx.strokeStyle = 'rgba(220,50,50,0.6)';
906
- ctx.lineWidth = 2; ctx.moveTo(x, ya.top); ctx.lineTo(x, ya.bottom); ctx.stroke();
907
- }
908
- // Per-segment mode indicators
909
- const data = chart.data.datasets[0].data;
910
- for (let i = 0; i < data.length - 1; i++) {
911
- const mode = data[i].mode;
912
- if (!mode) continue;
913
- const x0 = chart.scales.x.getPixelForValue(data[i].x);
914
- const y0 = chart.scales.y.getPixelForValue(data[i].y);
915
- const x1 = chart.scales.x.getPixelForValue(data[i+1].x);
916
- const y1 = chart.scales.y.getPixelForValue(data[i+1].y);
917
- const mx = (x0+x1)/2, my = (y0+y1)/2;
918
- ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2*Math.PI);
919
- ctx.fillStyle = '#ff8c00'; ctx.fill();
920
- ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
921
- ctx.fillStyle = '#fff';
922
- ctx.font = 'bold 8px sans-serif';
923
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
924
- ctx.fillText(mode[0], mx, my);
925
- }
926
- ctx.restore();
927
- },
928
- dragctx:{item:null, max_steps:()=>Math.max(1,+step_ele.value)},
929
- lastRemoved:false,
930
- startDrag(chart,args){const item=this.getItem(chart,args);if(item)this.dragctx.item=item;else this.addPoint(chart,args);},
931
- onDrag(chart,args){const y=this.getY(chart,args);const{datasetIndex,index}=this.dragctx.item;const item=chart.data.datasets[datasetIndex].data[index];if(item.y!==y){item.y=y;chart.update();}},
932
- endDrag(chart){this.dragctx.item=null;},
933
- addPoint(chart,args){const x=this.getX(chart,args),y=this.getY(chart,args);const found=chart.data.datasets[0].data.find(v=>v.x===x);if(found){if(found.y!==y){found.y=y;chart.update();}}else this.addItem(chart,{x,y});},
934
- removePoint(chart,args){const item_=this.getItem(chart,args);if(item_){const{datasetIndex,index}=item_;const item=chart.data.datasets[datasetIndex].data[index];if(1<item.x&&item.x<this.dragctx.max_steps()){this.lastRemoved=true;chart.data.datasets[datasetIndex].data.splice(index,1);chart.update();}}},
935
- getX(chart,args){const xx=chart.scales.x.getValueForPixel(args.event.native.clientX-chart.canvas.getBoundingClientRect().left);return Math.max(1,Math.min(Math.round(xx),this.dragctx.max_steps()));},
936
- getY(chart,args){const yy=chart.scales.y.getValueForPixel(args.event.native.clientY-chart.canvas.getBoundingClientRect().top);return Math.max(1,Math.min(Math.round(yy),this.dragctx.max_steps()));},
937
- getItem(chart,args){const items=chart.getElementsAtEventForMode(args.event,'nearest',{intersect:true},false);return items.length===0?null:items[0];},
938
- addItem(chart,xy,donotupdate){chart.data.datasets[0].data.push(xy);chart.data.datasets[0].data.sort((a,b)=>a.x-b.x);if(!donotupdate)chart.update();},
939
- }];
940
-
941
- return plugins;
942
- }
943
-
944
- function updateSteps(chart, max_steps) {
945
- max_steps = Math.max(2, max_steps);
946
- chart.options.scales.x.max = max_steps+1;
947
- chart.options.scales.y.max = max_steps+1;
948
- const data = chart.data.datasets[0].data;
949
- const new_data = [];
950
- for (const pt of data) if(1<=pt.x&&pt.x<=max_steps) new_data.push({...pt, y:Math.min(pt.y,max_steps)});
951
- if (!new_data.find(c=>c.x===1)) new_data.unshift({x:1, y:1});
952
- if (!new_data.find(c=>c.x===max_steps)) new_data.push ({x:max_steps,y:max_steps});
953
- chart.data.datasets[0].data = new_data;
954
- chart.update();
955
- }
956
-
957
- })('TimeMachine');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/scripts/timemachine.py DELETED
@@ -1,385 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import json
5
- import bisect
6
- from typing import Any, List, Callable
7
-
8
- import gradio as gr
9
-
10
- from modules.processing import StableDiffusionProcessing
11
- from modules import scripts, extensions, script_callbacks
12
- from fastapi.responses import JSONResponse
13
-
14
- from scripts.timemachinelib import sampler as sampler_utils
15
- from scripts.timemachinelib.xyz import init_xyz
16
-
17
- NAME = 'TimeMachine'
18
- _UNSUPPORTED = sampler_utils.UNSUPPORTED_SAMPLERS
19
-
20
-
21
- class Script(scripts.Script):
22
-
23
- def title(self):
24
- return NAME
25
-
26
- def show(self, is_img2img):
27
- return scripts.AlwaysVisible
28
-
29
- def ui(self, is_img2img):
30
- ext = _get_self_extension()
31
- if ext is not None and not is_img2img:
32
- js_ = [f'{x.path}?{os.path.getmtime(x.path)}'
33
- for x in ext.list_files('javascript/modules', '.js')]
34
- js_.insert(0, ext.path)
35
- gr.HTML(value='\n'.join(js_), elem_id=f'{NAME.lower()}-js_modules')
36
-
37
- mode = 'img2img' if is_img2img else 'txt2img'
38
- id_ = lambda x: f'{NAME.lower()}-{mode}-{x}'
39
- js_ = lambda s: f'globalThis["{id_(s)}"]'
40
-
41
- with gr.Accordion(NAME, open=False, elem_id=id_('accordion')):
42
- with gr.Row():
43
- enabled = gr.Checkbox(label='Enabled', value=False, elem_id=id_('enabled'))
44
- scheduler = gr.Dropdown(
45
- label='Scheduler', choices=sampler_utils.get_scheduler_choices(),
46
- value='Use sampler default', elem_id=id_('scheduler'))
47
- interp_mode = gr.Dropdown(
48
- label='Interpolation', choices=['Linear', 'Smooth (spline)', 'Step', 'Monotone', 'Ease In', 'Ease Out', 'Ease In-Out', 'Cubic', 'Exponential'],
49
- value='Linear', elem_id=id_('interpolation'))
50
-
51
- with gr.Row():
52
- cutoff_steps = gr.Number(
53
- label='Cutoff step (0=off)', value=0, minimum=0, maximum=150,
54
- step=1, elem_id=id_('cutoff'))
55
- hr_cutoff_cb = gr.Checkbox(
56
- label='HR Cutoff', value=True, elem_id=id_('hr_cutoff'))
57
- one_shot = gr.Checkbox(
58
- label='One-shot', value=False, elem_id=id_('one_shot'))
59
-
60
- gr.HTML(elem_id=id_('container'))
61
-
62
- with gr.Accordion('HR Fix Curve (txt2img only)', open=False,
63
- elem_id=id_('hr_accordion')):
64
- hr_enabled = gr.Checkbox(
65
- label='Use separate curve for HR pass', value=False,
66
- elem_id=id_('hr_enabled'))
67
- gr.HTML(elem_id=id_('hr_container'))
68
-
69
- with gr.Group(visible=False):
70
- sink = gr.HTML(value='')
71
- tm = _js2py('tm', id_, js_, sink)
72
- hr_sink = gr.HTML(value='')
73
- tm_hr = _js2py('tm_hr', id_, js_, hr_sink)
74
-
75
- return [enabled, scheduler, interp_mode, tm, hr_enabled, tm_hr, cutoff_steps, hr_cutoff_cb, one_shot]
76
-
77
- def process_batch(
78
- self,
79
- p: StableDiffusionProcessing,
80
- enabled: bool,
81
- scheduler: str,
82
- interp_mode: str,
83
- tm: str,
84
- hr_enabled: bool,
85
- tm_hr: str,
86
- cutoff_steps: int = 0,
87
- hr_cutoff_enabled: bool = True,
88
- one_shot: bool = False,
89
- **kwargs,
90
- ):
91
- if not enabled:
92
- return
93
- if not tm or not tm.strip():
94
- return
95
- if p.sampler_name in _UNSUPPORTED:
96
- msg = (f'⚠ {p.sampler_name!r} несовместим: '
97
- f'принимает sigma_min/max/n, не массив sigmas. '
98
- f'Используйте: Euler, DPM++ 2M, DPM++ SDE, Heun, LMS и др.')
99
- print(f'\n[TimeMachine] {msg}\n')
100
- gr.Warning(f'[TimeMachine] {msg}')
101
- return
102
-
103
- vs = _parse_points(tm)
104
- if vs is None:
105
- return
106
-
107
- if len(vs) < 2 or 1 not in vs or p.steps not in vs:
108
- points = [{'x': k, 'y': v['y'], 'mode': v.get('mode')} for k, v in sorted(vs.items())]
109
- msg = (f'Кривая должна содержать точки x=1 и x={p.steps} '
110
- f'(текущие x: {[p["x"] for p in points]})')
111
- print(f'[TimeMachine] {msg}')
112
- gr.Warning(f'[TimeMachine] {msg}')
113
- return
114
-
115
- steps_float = _interpolate(vs, p.steps, interp_mode)
116
- if len(steps_float) != p.steps:
117
- return
118
-
119
- cs = int(cutoff_steps)
120
- steps_float = _apply_cutoff(steps_float, cs, p.steps)
121
-
122
- if scheduler == 'Use sampler default':
123
- sched_name = sampler_utils.get_default_scheduler_for(p)
124
- else:
125
- from modules import sd_schedulers
126
- sched_name = next(
127
- (s.name for s in sd_schedulers.schedulers if s.label == scheduler),
128
- 'karras')
129
-
130
- override_main = sampler_utils.build_sigma_override(steps_float, sched_name)
131
- override_hr = _build_hr_override(p, hr_enabled, tm_hr, interp_mode, sched_name, cs, hr_cutoff_enabled)
132
-
133
- if override_hr is not None:
134
- def combined(n: int):
135
- return override_hr(n) if getattr(p, 'is_hr_pass', False) else override_main(n)
136
- p.sampler_noise_scheduler_override = combined
137
- else:
138
- p.sampler_noise_scheduler_override = override_main
139
-
140
- p._timemachine_override_set = True
141
- p.extra_generation_params.update({
142
- f'{NAME} Enabled': True,
143
- f'{NAME} Interpolation': interp_mode,
144
- f'{NAME} Steps': [round(t, 3) for t in steps_float],
145
- f'{NAME} Scheduler': sched_name,
146
- f'{NAME} Cutoff': cs,
147
- f'{NAME} One-shot': one_shot,
148
- })
149
-
150
- def postprocess_batch(self, p, *args, **kwargs):
151
- if getattr(p, '_timemachine_override_set', False):
152
- p.sampler_noise_scheduler_override = None
153
- p._timemachine_override_set = False
154
-
155
-
156
- # ── Интерполяция ──────────────────────────────────────────
157
-
158
- def _sliding_pairs(xs):
159
- for i in range(len(xs) - 1):
160
- yield xs[i], xs[i + 1]
161
-
162
-
163
- def _find_segment(xs: list, step: int) -> int:
164
- i = bisect.bisect_right(xs, step) - 1
165
- return max(0, min(i, len(xs) - 2))
166
-
167
-
168
- _WEIGHT_FUNCS: dict[str, Callable[[float], float]] = {
169
- 'Ease In': lambda t: t * t,
170
- 'Ease Out': lambda t: 1.0 - (1.0 - t) * (1.0 - t),
171
- 'Ease In-Out': lambda t: t * t * (3.0 - 2.0 * t),
172
- 'Cubic': lambda t: t ** 3,
173
- 'Exponential': lambda t: 2.0 ** t - 1.0,
174
- }
175
- _GLOBAL_MODES = frozenset({'Smooth (spline)', 'Step', 'Monotone'})
176
-
177
-
178
- def _interpolate_segments(vs: dict, n: int, default_mode: str = 'Linear', ignore_per_segment: bool = False) -> List[float]:
179
- default_weight_fn = _WEIGHT_FUNCS.get(default_mode, lambda t: t)
180
- steps: List[float] = []
181
- for min_x, max_x in _sliding_pairs(sorted(vs.keys())):
182
- min_y, max_y = float(vs[min_x]['y']), float(vs[max_x]['y'])
183
- seg_mode = default_mode if ignore_per_segment else (vs[min_x].get('mode') or default_mode)
184
- if seg_mode == 'Step':
185
- for step in range(min_x, max_x):
186
- steps.append(min_y)
187
- else:
188
- wfn = _WEIGHT_FUNCS.get(seg_mode, default_weight_fn)
189
- for step in range(min_x, max_x):
190
- t = (step - min_x) / (max_x - min_x)
191
- steps.append(min_y + wfn(t) * (max_y - min_y))
192
- steps.append(float(vs[n]['y']))
193
- return steps
194
-
195
-
196
- def _interpolate(vs: dict, n: int, mode: str) -> List[float]:
197
- if mode == 'Smooth (spline)': return _compute_remapped_steps_spline(vs, n)
198
- if mode == 'Step': return _compute_remapped_steps_step(vs, n)
199
- if mode == 'Monotone': return _compute_remapped_steps_monotone(vs, n)
200
- return _interpolate_segments(vs, n, mode)
201
-
202
-
203
- def _compute_remapped_steps_step(vs: dict, n: int) -> List[float]:
204
- return _interpolate_segments(vs, n, default_mode='Step', ignore_per_segment=True)
205
-
206
-
207
- def _compute_remapped_steps_monotone(vs: dict, n: int) -> List[float]:
208
- xs = sorted(vs.keys())
209
- ys = [float(vs[x]['y']) for x in xs]
210
- k = len(xs)
211
-
212
- if k == 2:
213
- return _interpolate_segments(vs, n, ignore_per_segment=True)
214
-
215
- h = [xs[i+1] - xs[i] for i in range(k-1)]
216
- delta = [(ys[i+1]-ys[i]) / h[i] for i in range(k-1)]
217
-
218
- m = [0.0] * k
219
- m[0] = delta[0]
220
- m[k-1] = delta[k-2]
221
- for i in range(1, k-1):
222
- p = (delta[i-1]*h[i] + delta[i]*h[i-1]) / (h[i-1]+h[i])
223
- if delta[i-1]*delta[i] <= 0:
224
- m[i] = 0.0
225
- else:
226
- sign = 1.0 if p >= 0 else -1.0
227
- m[i] = sign * min(abs(p), 2*abs(delta[i-1]), 2*abs(delta[i]))
228
-
229
- steps: List[float] = []
230
- for step in range(1, n+1):
231
- seg = _find_segment(xs, step)
232
- x0, x1 = xs[seg], xs[seg+1]
233
- h_s = x1 - x0
234
- t = max(0.0, min(1.0, (step-x0)/h_s if h_s>0 else 1.0))
235
- t2, t3 = t*t, t*t*t
236
- y = ((2*t3-3*t2+1)*ys[seg] + (t3-2*t2+t)*h_s*m[seg]
237
- + (-2*t3+3*t2)*ys[seg+1] + (t3-t2)*h_s*m[seg+1])
238
- steps.append(max(1.0, min(float(n), y)))
239
- return steps
240
-
241
-
242
- def _catmull_rom(t: float, p0: float, p1: float, p2: float, p3: float) -> float:
243
- t2 = t * t; t3 = t2 * t
244
- return 0.5 * (2*p1 + (-p0+p2)*t + (2*p0-5*p1+4*p2-p3)*t2 + (-p0+3*p1-3*p2+p3)*t3)
245
-
246
-
247
- def _compute_remapped_steps_spline(vs: dict, n: int) -> List[float]:
248
- xs = sorted(vs.keys()); ys = [float(vs[x]['y']) for x in xs]; k = len(xs)
249
- y_ext = [2*ys[0]-ys[1]] + ys + [2*ys[-1]-ys[-2]]
250
- steps: List[float] = []
251
- for step in range(1, n+1):
252
- seg = _find_segment(xs, step)
253
- x0, x1 = xs[seg], xs[seg+1]
254
- t = max(0., min(1., (step-x0)/(x1-x0) if x1>x0 else 1.))
255
- i = seg+1
256
- y = _catmull_rom(t, y_ext[i-1], y_ext[i], y_ext[i+1], y_ext[i+2])
257
- steps.append(max(1., min(float(n), y)))
258
- return steps
259
-
260
-
261
- def _apply_cutoff(sf: list[float], cs: int, total: int, blend: int = 4) -> list[float]:
262
- """Плавный cutoff: W=blend шагов смешиваем кривую → identity, дальше чистая identity."""
263
- if cs <= 0 or cs >= total:
264
- return sf
265
- out = sf[:]
266
- blend_end = min(cs + blend, total)
267
- blend_steps = blend_end - cs
268
- for i in range(cs, blend_end):
269
- alpha = (i - cs + 1) / blend_steps if blend_steps > 0 else 1.0
270
- out[i] = out[i] * (1.0 - alpha) + float(i + 1) * alpha
271
- for i in range(blend_end, total):
272
- out[i] = float(i + 1)
273
- return out
274
-
275
-
276
- def _build_hr_override(p, hr_enabled: bool, tm_hr: str, interp_mode: str, sched_name: str,
277
- cutoff_steps: int = 0, hr_cutoff_enabled: bool = True):
278
- """Строит override для HR-прохода если все условия выполнены."""
279
- if not hr_enabled:
280
- return None
281
- if not getattr(p, 'enable_hr', False):
282
- return None
283
- if not tm_hr or not tm_hr.strip():
284
- return None
285
-
286
- hr_steps = getattr(p, 'hr_second_pass_steps', 0)
287
- if hr_steps <= 0:
288
- hr_steps = p.steps
289
-
290
- vs_hr = _parse_points(tm_hr)
291
- if vs_hr is None:
292
- return None
293
-
294
- if len(vs_hr) < 2 or 1 not in vs_hr or hr_steps not in vs_hr:
295
- msg = (f'HR кривая должна содержать x=1 и x={hr_steps} '
296
- f'(x: {sorted(vs_hr.keys())})')
297
- print(f'[TimeMachine] {msg}')
298
- gr.Warning(f'[TimeMachine] {msg}')
299
- return None
300
-
301
- sf = _interpolate(vs_hr, hr_steps, interp_mode)
302
- if len(sf) != hr_steps:
303
- return None
304
-
305
- if hr_cutoff_enabled and cutoff_steps > 0:
306
- if hr_steps <= cutoff_steps:
307
- hr_cs = max(1, round(cutoff_steps * hr_steps / p.steps))
308
- else:
309
- hr_cs = cutoff_steps
310
- sf = _apply_cutoff(sf, hr_cs, hr_steps)
311
-
312
- return sampler_utils.build_sigma_override(sf, sched_name)
313
-
314
-
315
- # ── Вспомогательные ──────────────────────────────────────
316
-
317
- def _parse_points(json_str: str) -> dict[int, dict] | None:
318
- try:
319
- pts = json.loads(json_str)
320
- except json.JSONDecodeError:
321
- return None
322
- if not isinstance(pts, list):
323
- return None
324
- try:
325
- result: dict[int, dict] = {}
326
- for v in pts:
327
- if isinstance(v, dict) and 'x' in v and 'y' in v:
328
- x = int(v['x'])
329
- result[x] = {
330
- 'y': int(v['y']),
331
- 'mode': v.get('mode') or None,
332
- }
333
- return result
334
- except (KeyError, TypeError, ValueError):
335
- return None
336
-
337
-
338
- def _get_self_extension():
339
- self_path = os.path.abspath(__file__)
340
- for ext in extensions.active():
341
- if self_path.startswith(os.path.abspath(ext.path) + os.sep):
342
- return ext
343
-
344
-
345
- def _js2py(name: str, id_: Callable, js_: Callable, sink: Any):
346
- v_set = gr.Button(elem_id=id_(f'{name}_set'))
347
- v = gr.Textbox(elem_id=id_(name))
348
- v_sink = gr.Textbox()
349
- v_set.click(fn=None, _js=js_(name), outputs=[v, v_sink])
350
- v_sink.change(fn=None, _js=js_(f'{name}_after'), outputs=[sink])
351
- return v
352
-
353
-
354
- init_xyz(Script, NAME)
355
- sampler_utils.register_ni_samplers()
356
-
357
-
358
- # ── FastAPI: real sigmas for JS ──────────────────────────────
359
-
360
- def _resolve_scheduler_label(label: str) -> str:
361
- """Конвертирует label из UI во внутреннее имя."""
362
- if label in ('Use sampler default', 'Automatic', 'automatic'):
363
- return label
364
- from modules import sd_schedulers
365
- return next((s.name for s in sd_schedulers.schedulers if s.label == label), 'karras')
366
-
367
-
368
- def _on_app_started(demo, app):
369
- @app.get("/timemachine/sigmas")
370
- def get_sigmas(scheduler: str = 'karras', steps: int = 20,
371
- sampler: str | None = None, a1111_scheduler: str | None = None):
372
- try:
373
- if scheduler == 'Use sampler default':
374
- if a1111_scheduler and a1111_scheduler != 'Automatic':
375
- scheduler = _resolve_scheduler_label(a1111_scheduler)
376
- else:
377
- scheduler = sampler_utils.get_default_scheduler_for(
378
- sampler_name=sampler, scheduler_name='Automatic')
379
- sigmas = sampler_utils.compute_sigmas(steps, scheduler)
380
- return {"sigmas": sigmas.tolist()}
381
- except Exception as e:
382
- print(f'[TimeMachine] sigmas API error: {e}')
383
- return JSONResponse(content={"sigmas": []}, status_code=500)
384
-
385
- script_callbacks.on_app_started(_on_app_started)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/scripts/timemachinelib/__init__.py DELETED
@@ -1,16 +0,0 @@
1
- from .sampler import (
2
- build_sigma_override,
3
- compute_sigmas,
4
- get_default_scheduler_for,
5
- get_scheduler_choices,
6
- register_ni_samplers,
7
- UNSUPPORTED_SAMPLERS,
8
- )
9
- from .xyz import init_xyz
10
-
11
- __all__ = [
12
- 'build_sigma_override', 'compute_sigmas',
13
- 'get_default_scheduler_for', 'get_scheduler_choices',
14
- 'register_ni_samplers', 'UNSUPPORTED_SAMPLERS',
15
- 'init_xyz',
16
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/scripts/timemachinelib/sampler.py DELETED
@@ -1,299 +0,0 @@
1
- # timemachinelib/sampler.py — v3.1 (bugfix)
2
- #
3
- # Исправления:
4
- # - scheduler.need_inner_model → getattr (AttributeError на нестандартных планировщиках)
5
- # - scheduler.default_rho → getattr
6
- # - opts.rho/sigma_min/sigma_max/use_old_karras → getattr (отсутствуют в ряде версий)
7
- # - Удалён неиспользуемый Optional
8
-
9
- from __future__ import annotations
10
-
11
- import math
12
-
13
- import torch
14
- from modules import sd_schedulers, devices
15
- from modules.shared import opts
16
-
17
-
18
- def _build_model_wrap():
19
- """k-diffusion denoiser wrapper. Поддерживает SD1/SD2/SDXL/SD3."""
20
- from modules import shared
21
- import k_diffusion.external
22
-
23
- sd_model = shared.sd_model
24
- if hasattr(sd_model, 'create_denoiser'):
25
- return sd_model.create_denoiser()
26
-
27
- param = getattr(sd_model, 'parameterization', 'eps')
28
- cls = (k_diffusion.external.CompVisVDenoiser
29
- if param == 'v' else k_diffusion.external.CompVisDenoiser)
30
- quantize = getattr(opts, 'enable_quantization', False)
31
- return cls(sd_model, quantize=quantize)
32
-
33
-
34
- def compute_sigmas(n: int, scheduler_name: str, model_wrap=None) -> torch.Tensor:
35
- """Генерирует n+1 сигм (убывающие, последний = 0)."""
36
- if model_wrap is None:
37
- model_wrap = _build_model_wrap()
38
-
39
- m_sigma_min = model_wrap.sigmas[0].item()
40
- m_sigma_max = model_wrap.sigmas[-1].item()
41
-
42
- # BUG-FIX: getattr вместо прямого обращения — атрибуты могут отсутствовать
43
- if getattr(opts, 'use_old_karras_scheduler_sigmas', False):
44
- sigma_min, sigma_max = 0.1, 10.0
45
- else:
46
- raw_min = getattr(opts, 'sigma_min', 0)
47
- raw_max = getattr(opts, 'sigma_max', 0)
48
- sigma_min = raw_min if raw_min != 0 else m_sigma_min
49
- sigma_max = raw_max if raw_max != 0 else m_sigma_max
50
-
51
- scheduler = sd_schedulers.schedulers_map.get(scheduler_name)
52
- if scheduler is None or scheduler.function is None:
53
- return model_wrap.get_sigmas(n).cpu()
54
-
55
- kwargs: dict = {'sigma_min': sigma_min, 'sigma_max': sigma_max, 'device': devices.cpu}
56
-
57
- # BUG-FIX: getattr с fallback — у нестандартных планировщиков атрибут может отсутствовать
58
- if getattr(scheduler, 'need_inner_model', False):
59
- kwargs['inner_model'] = model_wrap
60
-
61
- default_rho = getattr(scheduler, 'default_rho', -1)
62
- rho = getattr(opts, 'rho', 0)
63
- if default_rho != -1 and rho != 0 and rho != default_rho:
64
- kwargs['rho'] = rho
65
-
66
- return scheduler.function(n=n, **kwargs).cpu()
67
-
68
-
69
- def _lerp_sigma(full_sigmas: torch.Tensor, t: float) -> torch.Tensor:
70
- """
71
- Линейная интерполяция сигмы для дробной позиции t ∈ [1.0, max_idx].
72
- t=1.0 → full_sigmas[0] (макс. шум)
73
- t=max_idx → full_sigmas[max_idx-1] (мин. ненулевой шум)
74
- """
75
- n = len(full_sigmas) - 1 # кол-во ненулевых позиций
76
- lo = max(0, int(math.floor(t)) - 1) # 0-based нижняя граница
77
- hi = min(lo + 1, n - 1) # 0-based верхняя (не выходим за 0-элемент)
78
- frac = t - math.floor(t)
79
- return full_sigmas[lo] * (1.0 - frac) + full_sigmas[hi] * frac
80
-
81
-
82
- def build_sigma_override(steps_float: list[float], scheduler_name: str):
83
- """
84
- Возвращает функцию для p.sampler_noise_scheduler_override.
85
-
86
- steps_float — float позиции (1-based) в sigma-расписании, одна на каждый UI-шаг.
87
- scheduler_name — имя планировщика ('karras', 'exponential', ...)
88
- """
89
- max_idx = math.ceil(max(steps_float))
90
-
91
- def override(n: int) -> torch.Tensor:
92
- """n = p.steps (или +1 при discard_next_to_last_sigma)."""
93
- model_wrap = _build_model_wrap()
94
- full_sigmas = compute_sigmas(max_idx, scheduler_name, model_wrap)
95
-
96
- selected = [_lerp_sigma(full_sigmas, t) for t in steps_float]
97
-
98
- delta = n - len(selected)
99
- if delta > 0:
100
- last = selected[-1]
101
- min_sig = full_sigmas[-2] # минимальная ненулевая
102
- for i in range(1, delta + 1):
103
- alpha = i / (delta + 1)
104
- selected.append(last * (1.0 - alpha) + min_sig * alpha)
105
- elif delta < 0:
106
- selected = selected[:n]
107
-
108
- return torch.cat([torch.stack(selected), full_sigmas[-1:]])
109
-
110
- return override
111
-
112
-
113
- def get_default_scheduler_for(p=None, *, sampler_name=None, scheduler_name=None, is_hr_pass=False) -> str:
114
- """Резолвит scheduler: сначала p.scheduler (реальный выбор Schedule type),
115
- только если 'Automatic' — дефолт, зашитый в конфиге сэмплера.
116
- Реплицирует modules/sd_samplers_kdiffusion.py:86-88."""
117
- if p is not None:
118
- scheduler_name = (p.hr_scheduler if is_hr_pass else p.scheduler) or 'Automatic'
119
- sampler_name = p.sampler_name
120
- sched = scheduler_name or 'Automatic'
121
- if sched == 'Automatic' and sampler_name:
122
- from modules import sd_samplers
123
- config = sd_samplers.find_sampler_config(sampler_name)
124
- sched = (config.options.get('scheduler') if config else None) or 'karras'
125
- return sched
126
-
127
-
128
- def get_scheduler_choices() -> list[str]:
129
- """Список планировщиков для UI-дропдауна."""
130
- skip = {'automatic', 'Automatic'}
131
- return ['Use sampler default'] + [
132
- s.label
133
- for s in sd_schedulers.schedulers
134
- if s.name not in skip and s.label not in skip and s.function is not None
135
- ]
136
-
137
-
138
- UNSUPPORTED_SAMPLERS = {'DPM fast', 'DPM adaptive'}
139
-
140
-
141
- # ═══════════════════════════════════════════════════════════════
142
- # Noise Injection (NI) семплеры
143
- #
144
- # Физическая идея: когда σ растёт между шагами (Time Travel),
145
- # латент находится «слишком чистым» для текущего уровня шума.
146
- # Правильное решение — добавить шум: x += √(σ²_{k+1} − σ²_k) · ε
147
- #
148
- # Кастомные циклы обязательны: стандартные сэмплеры k-diffusion
149
- # не знают о noise injection, и обёртка модели не может обновить
150
- # x во внешней функции (см. NIModelWrapper — удалена как ошибочная).
151
- # ═══════════════════════════════════════════════════════════════
152
-
153
- import tqdm as _tqdm
154
- from modules import sd_samplers, sd_samplers_common
155
- import modules.sd_samplers_kdiffusion as K
156
-
157
-
158
- class TimeMachineEulerNI:
159
- """
160
- Euler + Noise Injection. Кастомный цикл — NI модифицирует x
161
- напрямую, затем вычисляется стандартный Euler-шаг.
162
- """
163
- @torch.no_grad()
164
- def __call__(self, model, x, sigmas, extra_args=None, callback=None, disable=None,
165
- s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1., **kwargs):
166
- extra_args = extra_args or {}
167
- s_in = x.new_ones([x.shape[0]])
168
- last_sigma: float | None = None
169
- n_steps = len(sigmas) - 1
170
-
171
- for i in _tqdm.tqdm(range(n_steps), disable=disable):
172
- sigma_curr = sigmas[i]
173
- sigma_next = sigmas[i + 1]
174
-
175
- # ── Noise Injection (σ выросла → добавить шум) ──────────
176
- sv = sigma_curr.item()
177
- if last_sigma is not None and sv > last_sigma:
178
- delta_var = sv ** 2 - last_sigma ** 2
179
- if delta_var > 0:
180
- x = x + torch.randn_like(x) * math.sqrt(delta_var)
181
- last_sigma = sv
182
-
183
- # ── s_churn (ε-буст для exploration) ─────────────────────
184
- gamma = min(s_churn / n_steps, math.sqrt(2) - 1) if s_tmin <= sv <= s_tmax else 0.
185
- sigma_hat = sigma_curr * (gamma + 1)
186
- if gamma > 0:
187
- eps = torch.randn_like(x) * s_noise
188
- x = x + eps * math.sqrt(sigma_hat ** 2 - sv ** 2)
189
-
190
- # ── Euler шаг ────────────────────────────────────────────
191
- denoised = model(x, sigma_hat * s_in, **extra_args)
192
- if callback is not None:
193
- callback({'x': x, 'i': i, 'sigma': sigma_hat,
194
- 'sigma_hat': sigma_hat, 'denoised': denoised})
195
-
196
- d = (x - denoised) / sigma_hat
197
- dt = sigma_next - sigma_hat
198
- x = x + d * dt
199
-
200
- return x
201
-
202
-
203
- class TimeMachineDPM2MNI:
204
- """
205
- DPM++ 2M + Noise Injection.
206
-
207
- Кастомный цикл: при NI кеш old_denoised сбрасывается,
208
- иначе второй порядок использует значение с неверного состояния.
209
- """
210
- @torch.no_grad()
211
- def __call__(self, model, x, sigmas, extra_args=None, callback=None, disable=None, **kwargs):
212
- extra_args = extra_args or {}
213
- s_in = x.new_ones([x.shape[0]])
214
- sigma_fn = lambda t: t.neg().exp()
215
- t_fn = lambda s: s.log().neg()
216
- old_denoised = None
217
- last_sigma: float | None = None
218
-
219
- for i in _tqdm.tqdm(range(len(sigmas) - 1), disable=disable):
220
- sigma_curr = sigmas[i]
221
- sigma_next = sigmas[i + 1]
222
-
223
- # ── Noise Injection ───────────────────────────��──────────
224
- sv = sigma_curr.item()
225
- if last_sigma is not None and sv > last_sigma:
226
- delta_var = sv ** 2 - last_sigma ** 2
227
- if delta_var > 0:
228
- x = x + torch.randn_like(x) * math.sqrt(delta_var)
229
- old_denoised = None # кеш невалиден после re-noise
230
- last_sigma = sv
231
-
232
- # ── DPM++ 2M шаг ─────────────────────────────────────────
233
- denoised = model(x, sigma_curr * s_in, **extra_args)
234
- if callback is not None:
235
- callback({'x': x, 'i': i, 'sigma': sigma_curr,
236
- 'sigma_hat': sigma_curr, 'denoised': denoised})
237
-
238
- t, t_next = t_fn(sigma_curr), t_fn(sigma_next)
239
- h = t_next - t
240
-
241
- if old_denoised is None or sigma_next == 0 or h == 0:
242
- # Euler: первый шаг, после NI-сброса, последний шаг,
243
- # или flat sigma (h=0 — деление на ноль в 2M формулах)
244
- x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised
245
- else:
246
- # DPM++ 2M второй порядок
247
- h_last = t - t_fn(sigmas[i - 1])
248
- r = h_last / h
249
- denoised_d = (1 + 1 / (2 * r)) * denoised - (1 / (2 * r)) * old_denoised
250
- x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_d
251
-
252
- old_denoised = denoised
253
-
254
- return x
255
-
256
-
257
- # ─────────────────────────────────────────────────────────────────
258
- # Регистрация NI семплеров в A1111 с поддержкой s_churn
259
- # ─────────────────────────────────────────────────────────────────
260
-
261
- class _TMSampler(K.KDiffusionSampler):
262
- """
263
- Обёртка над KDiffusionSampler, которая форсирует extra_params
264
- для s_churn/s_tmin/s_tmax/s_noise (родной A1111 маппинг
265
- sampler_extra_params не работает для TM-сэмплеров, т.к.
266
- ключуется по строковому имени функции, а у нас — объект).
267
- """
268
- def __init__(self, func, sd_model, options=None):
269
- super().__init__(func, sd_model, options)
270
- self.extra_params = ['s_churn', 's_tmin', 's_tmax', 's_noise']
271
-
272
-
273
- def register_ni_samplers() -> None:
274
- """
275
- Добавляет 'TM Euler (NI)' и 'TM DPM++ 2M (NI)' в список семплеров A1111.
276
- Используются совместно с TimeMachine: sigma override задаёт расписание,
277
- NI добавляет физически правильный шум при backward-шагах.
278
- Без TimeMachine ведут себя идентично Euler / DPM++ 2M (NI не срабатывает).
279
- """
280
- ni_entries = [
281
- ('TM Euler (NI)', TimeMachineEulerNI(), ['k_tm_euler_ni'], {}),
282
- ('TM DPM++ 2M (NI)', TimeMachineDPM2MNI(), ['k_tm_dpmpp2m_ni'], {'scheduler': 'karras'}),
283
- ]
284
- added = False
285
- for label, func, aliases, options in ni_entries:
286
- if label not in [x.name for x in sd_samplers.all_samplers]:
287
- # default-аргументы f= и o= фиксируют текущие значения в closure
288
- data = sd_samplers_common.SamplerData(
289
- label,
290
- lambda model, f=func, o=options: _TMSampler(f, model, options=o),
291
- aliases,
292
- options,
293
- )
294
- sd_samplers.all_samplers.append(data)
295
- added = True
296
-
297
- if added:
298
- sd_samplers.set_samplers()
299
- sd_samplers.all_samplers_map = {x.name: x for x in sd_samplers.all_samplers}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/scripts/timemachinelib/xyz.py DELETED
@@ -1,74 +0,0 @@
1
- # xyz.py — v7: индексы под UI с 9 компонентами
2
- # [enabled, scheduler, interp_mode, tm, hr_enabled, tm_hr, cutoff_steps, hr_cutoff, one_shot]
3
- # 0=enabled 1=scheduler 2=interp 4=hr_enabled 6=cutoff_steps 7=hr_cutoff 8=one_shot
4
-
5
- import os
6
-
7
- from modules import scripts
8
- from modules.processing import StableDiffusionProcessingTxt2Img
9
-
10
-
11
- def __set_value(p, script, index, value):
12
- args = list(p.script_args)
13
- all_s = (scripts.scripts_txt2img.scripts if isinstance(p, StableDiffusionProcessingTxt2Img)
14
- else scripts.scripts_img2img.scripts)
15
- for idx in [x.args_from for x in all_s if isinstance(x, script)]:
16
- if idx is not None:
17
- args[idx + index] = value
18
- p.script_args = type(p.script_args)(args)
19
-
20
-
21
- def to_bool(v: str):
22
- if not v: return False
23
- v = v.strip().lower()
24
- if v == 'true': return True
25
- if v == 'false': return False
26
- try: return bool(int(v))
27
- except (ValueError, TypeError):
28
- raise ValueError('value must be True/False/1/0')
29
-
30
-
31
- class AxisOptions:
32
- def __init__(self, AxisOption, axis_options):
33
- self.AxisOption = AxisOption; self.target = axis_options; self.options = []
34
- def __enter__(self): self.options.clear(); return self
35
- def __exit__(self, *_):
36
- for opt in self.options: self.target.append(opt)
37
- self.options.clear()
38
- def create(self, name, type_fn, action, choices=None):
39
- return (self.AxisOption(name, type_fn, action, choices=lambda: choices)
40
- if choices else self.AxisOption(name, type_fn, action))
41
- def add(self, opt): self.options.append(opt)
42
-
43
-
44
- _xyz_registered = False
45
-
46
- def init_xyz(script, ext_name):
47
- global _xyz_registered
48
- if _xyz_registered: return
49
- for data in scripts.scripts_data:
50
- if os.path.basename(data.path) not in ('xy_grid.py','xyz_grid.py'): continue
51
- if not hasattr(data.module,'AxisOption') or not hasattr(data.module,'axis_options'): continue
52
- AO = data.module.AxisOption; ao = data.module.axis_options
53
- if not isinstance(AO, type) or not isinstance(ao, list): continue
54
- try: _create_options(script, ext_name, AO, ao)
55
- except Exception as e:
56
- print(f'[TimeMachine] Не удалось зарегистрировать XYZ-опции: {e}')
57
- _xyz_registered = True
58
-
59
-
60
- def _create_options(script, ext_name, AO, ao):
61
- from scripts.timemachinelib.sampler import get_scheduler_choices
62
- with AxisOptions(AO, ao) as opts:
63
- def define(param, index, type_fn, choices=None):
64
- def fn(p, x, xs): __set_value(p, script, index, x)
65
- return opts.create(f'{ext_name} {param}', type_fn, fn, choices)
66
- for opt in [
67
- define('Enabled', 0, to_bool, ['false','true']),
68
- define('Scheduler', 1, str, get_scheduler_choices()),
69
- define('Interpolation', 2, str, ['Linear','Smooth (spline)','Step','Monotone','Ease In','Ease Out','Ease In-Out','Cubic','Exponential']),
70
- define('HR Enabled', 4, to_bool, ['false','true']),
71
- define('Cutoff Steps', 6, int),
72
- define('HR Cutoff', 7, to_bool, ['false','true']),
73
- define('One-shot', 8, to_bool, ['false','true']),
74
- ]: opts.add(opt)