dikdimon commited on
Commit
0905af4
·
verified ·
1 Parent(s): c5e6503

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,951 +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
-
181
- async function _fetchRealSigmas(scheduler, steps) {
182
- try {
183
- let url = `/timemachine/sigmas?scheduler=${encodeURIComponent(scheduler)}&steps=${steps}`;
184
- const sampler = getSampler();
185
- if (sampler) url += `&sampler=${encodeURIComponent(sampler)}`;
186
- const res = await fetch(url);
187
- if (!res.ok) return null;
188
- const data = await res.json();
189
- return Array.isArray(data.sigmas) ? data.sigmas : null;
190
- } catch(e) {
191
- return null;
192
- }
193
- }
194
-
195
- function _karrasApprox(n) {
196
- const [sMin, sMax, rho] = [0.1, 14.6, 7];
197
- const sig = Array.from({length: n}, (_, i) => {
198
- const t = i / Math.max(1, n-1);
199
- return Math.pow(Math.pow(sMax,1/rho)+t*(Math.pow(sMin,1/rho)-Math.pow(sMax,1/rho)), rho);
200
- });
201
- sig.push(0);
202
- return sig;
203
- }
204
-
205
- function updateSigmaApprox(n, scheduler) {
206
- approxSigmas = _karrasApprox(n);
207
- _fetchRealSigmas(scheduler, n).then(real => {
208
- if (real && real.length >= n) {
209
- // real has n+1 sigmas (including trailing 0)
210
- approxSigmas = real.slice(0, n + 1);
211
- if (showSigma) { chart?.update(); if (hr_chart) hr_chart?.update(); }
212
- }
213
- });
214
- }
215
-
216
- // Динамические callbacks Chart.js
217
- chart.options.scales.y.ticks.callback = v => {
218
- if (!showSigma || v < 1 || v > approxSigmas.length-1) return v;
219
- const s = approxSigmas[Math.round(v)-1];
220
- return s == null ? v : s < 0.01 ? '0' : s < 1 ? s.toFixed(3) : s.toFixed(1);
221
- };
222
- chart.options.plugins.tooltip.callbacks.title = ctxs => ctxs.map(c => {
223
- const pos = c.parsed.y;
224
- if (!showSigma) return `${c.parsed.x} → ${pos}`;
225
- const s = approxSigmas[Math.round(pos)-1];
226
- return `step ${c.parsed.x} → ${pos}${s != null ? ` (σ≈${s.toFixed(3)})` : ''}`;
227
- });
228
-
229
- // Interp tension
230
- function getInterpMode() {
231
- return $$(`#${id('interpolation')} select`)?.value || 'Linear';
232
- }
233
- function updateChartStyle(c = chart) {
234
- const mode = getInterpMode();
235
- const ds = c.data.datasets[0];
236
- if (mode === 'Step') {
237
- ds.stepped = 'before'; ds.cubicInterpolationMode = 'default'; ds.tension = 0;
238
- } else if (mode === 'Monotone') {
239
- ds.stepped = false; ds.cubicInterpolationMode = 'monotone'; ds.tension = 0;
240
- } else if (mode === 'Smooth (spline)') {
241
- ds.stepped = false; ds.cubicInterpolationMode = 'default'; ds.tension = 0.4;
242
- } else {
243
- ds.stepped = false; ds.cubicInterpolationMode = 'default'; ds.tension = 0;
244
- }
245
- c.update();
246
- }
247
- const interpEl = $$(`#${id('interpolation')} select`);
248
- if (interpEl) interpEl.addEventListener('change', () => updateChartStyle());
249
- const schedEl = $$(`#${id('scheduler')} select`);
250
- if (schedEl) schedEl.addEventListener('change', () => {
251
- updateSigmaApprox(+step_ele.value, getScheduler());
252
- });
253
- const samplerEl = $$(`#${mode}_sampler select`);
254
- if (samplerEl) samplerEl.addEventListener('change', () => {
255
- updateSigmaApprox(+step_ele.value, getScheduler());
256
- });
257
-
258
- // ── История (Undo/Redo) ──────────────────────────────
259
- const MAX_HIST = 20;
260
- let hist = [], histIdx = -1;
261
-
262
- function saveState(c) {
263
- const snap = JSON.stringify(c.data.datasets[0].data);
264
- if (hist[histIdx] === snap) return;
265
- hist = hist.slice(0, histIdx+1);
266
- hist.push(snap);
267
- if (hist.length > MAX_HIST) hist.shift(); else histIdx++;
268
- }
269
- function undo(c) {
270
- if (histIdx > 0) {
271
- histIdx--;
272
- c.data.datasets[0].data = JSON.parse(hist[histIdx]);
273
- c.update(); triggerSync(c);
274
- }
275
- }
276
- function redo(c) {
277
- if (histIdx < hist.length-1) {
278
- histIdx++;
279
- c.data.datasets[0].data = JSON.parse(hist[histIdx]);
280
- c.update(); triggerSync(c);
281
- }
282
- }
283
-
284
- document.addEventListener('keydown', e => {
285
- const tag = document.activeElement?.tagName;
286
- if (tag==='INPUT'||tag==='TEXTAREA'||document.activeElement?.contentEditable==='true') return;
287
- if (!enabled?.checked) return;
288
- if (e.ctrlKey && e.key==='z' && !e.shiftKey) { e.preventDefault(); undo(chart); }
289
- if (e.ctrlKey && ((e.key==='z' && e.shiftKey)||e.key==='y')) { e.preventDefault(); redo(chart); }
290
- });
291
-
292
- // ── Sync + Persist ──────────────────────────────────
293
- let syncedData = null, pendingSync = null;
294
-
295
- function _saveToDisk() {
296
- try {
297
- localStorage.setItem(`tm_curve:${mode}`, JSON.stringify(chart.data.datasets[0].data));
298
- if (hr_chart) {
299
- localStorage.setItem(`tm_curve:${mode}:hr`, JSON.stringify(hr_chart.data.datasets[0].data));
300
- }
301
- } catch(e) { /* quota exceeded, silently ignore */ }
302
- }
303
-
304
- function _loadFromDisk() {
305
- const saved = localStorage.getItem(`tm_curve:${mode}`);
306
- if (saved) {
307
- try {
308
- const pts = JSON.parse(saved);
309
- if (Array.isArray(pts) && pts.length >= 2) {
310
- chart.data.datasets[0].data = pts;
311
- updateSteps(chart, +step_ele.value);
312
- syncedData = JSON.stringify(chart.data.datasets[0].data);
313
- return;
314
- }
315
- } catch(e) { /* ignore corrupt data */ }
316
- }
317
- }
318
-
319
- function triggerSync(c, key='tm') {
320
- const data = JSON.stringify(c.data.datasets[0].data);
321
- if (key==='tm' && data===syncedData) return;
322
- if (key==='tm_hr' && data===hr_syncedData) return;
323
- const prom = lib.js2py(mode, key, data).then(() => {
324
- if (key==='tm') { syncedData = data; pendingSync = null; }
325
- if (key==='tm_hr') { hr_syncedData = data; hr_pendingSync = null; }
326
- }).catch(() => {
327
- if (key==='tm') pendingSync = null;
328
- if (key==='tm_hr') hr_pendingSync = null;
329
- });
330
- if (key==='tm') pendingSync = prom;
331
- if (key==='tm_hr') hr_pendingSync = prom;
332
- _saveToDisk();
333
- }
334
-
335
- // Hook плагина
336
- const plugin = plugins[0];
337
- const _oEnd = plugin.endDrag.bind(plugin);
338
- plugin.endDrag = (c,a)=>{ _oEnd(c,a); saveState(c); triggerSync(c); };
339
- const _oAdd = plugin.addPoint.bind(plugin);
340
- plugin.addPoint = (c,a)=>{ _oAdd(c,a); saveState(c); triggerSync(c); };
341
- const _oDel = plugin.removePoint.bind(plugin);
342
- plugin.removePoint = (c,a)=>{ _oDel(c,a); saveState(c); triggerSync(c); };
343
-
344
- // Инициализация
345
- updateSteps(chart, +step_ele.value);
346
- updateSigmaApprox(+step_ele.value, getScheduler());
347
- _loadFromDisk();
348
- saveState(chart);
349
-
350
- let debounceTimer;
351
- step_ele.addEventListener('input', () => {
352
- updateSteps(chart, +step_ele.value);
353
- updateSigmaApprox(+step_ele.value, getScheduler());
354
- saveState(chart);
355
- if (cutoffEl) cutoffEl.setAttribute('max', +step_ele.value);
356
- clearTimeout(debounceTimer);
357
- debounceTimer = setTimeout(() => triggerSync(chart), 150);
358
- });
359
-
360
- // Обновление линии cutoff при изменении значения
361
- if (cutoffEl) {
362
- cutoffEl.addEventListener('input', () => {
363
- chart.update();
364
- if (hr_chart) hr_chart.update();
365
- });
366
- }
367
-
368
- // ── HR Fix график ────────────────────────────────────
369
- let hr_chart = null, hr_syncedData = null, hr_pendingSync = null;
370
- const hr_container = $$('#' + id('hr_container'));
371
-
372
- if (hr_container) {
373
- const hr_step_raw = $$(`#${mode}_hires_steps input[type=number]`);
374
- const hr_step = hr_step_raw || step_ele;
375
-
376
- const hr_canvas = document.createElement('canvas');
377
- hr_canvas.width = 512; hr_canvas.height = 512;
378
- const hr_plugins = createPlugins(hr_canvas, hr_step, cutoffEl);
379
-
380
- hr_chart = new Chart(hr_canvas.getContext('2d'), {
381
- type: 'scatter', data: createInitialData(),
382
- options: createChartOption(), plugins: hr_plugins,
383
- });
384
-
385
- hr_chart.options.scales.y.ticks.callback = v => {
386
- if (!showSigma || v < 1 || v > approxSigmas.length-1) return v;
387
- const s = approxSigmas[Math.round(v)-1];
388
- return s == null ? v : s < 1 ? s.toFixed(3) : s.toFixed(1);
389
- };
390
- hr_chart.options.plugins.tooltip.callbacks.title = ctxs => ctxs.map(c => {
391
- const pos = c.parsed.y;
392
- if (!showSigma) return `HR step ${c.parsed.x} → ${pos}`;
393
- const s = approxSigmas[Math.round(pos)-1];
394
- return `HR step ${c.parsed.x} → ${pos}${s != null ? ` (σ≈${s.toFixed(3)})` : ''}`;
395
- });
396
-
397
- hr_container.appendChild(hr_canvas);
398
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
399
-
400
- // Load persisted HR curve
401
- const savedHr = localStorage.getItem(`tm_curve:${mode}:hr`);
402
- if (savedHr) {
403
- try {
404
- const pts = JSON.parse(savedHr);
405
- if (Array.isArray(pts) && pts.length >= 2) {
406
- hr_chart.data.datasets[0].data = pts;
407
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
408
- hr_syncedData = JSON.stringify(hr_chart.data.datasets[0].data);
409
- }
410
- } catch(e) { /* ignore */ }
411
- }
412
-
413
- const hr_plugin = hr_plugins[0];
414
- const _hoEnd = hr_plugin.endDrag.bind(hr_plugin);
415
- hr_plugin.endDrag = (c,a)=>{ _hoEnd(c,a); triggerSync(c,'tm_hr'); };
416
- const _hoAdd = hr_plugin.addPoint.bind(hr_plugin);
417
- hr_plugin.addPoint = (c,a)=>{ _hoAdd(c,a); triggerSync(c,'tm_hr'); };
418
- const _hoDel = hr_plugin.removePoint.bind(hr_plugin);
419
- hr_plugin.removePoint = (c,a)=>{ _hoDel(c,a); triggerSync(c,'tm_hr'); };
420
-
421
- hr_step.addEventListener('input', () => {
422
- updateSteps(hr_chart, +hr_step.value || +step_ele.value);
423
- triggerSync(hr_chart, 'tm_hr');
424
- });
425
-
426
- // Синхронизируем tension HR графика с основным
427
- if (interpEl) interpEl.addEventListener('change', () => updateChartStyle(hr_chart));
428
- }
429
-
430
- // ── Generate button ─────────────────────────────────
431
- let generating = false;
432
- gradioApp().addEventListener('click', async e => {
433
- if (e.target !== generate_button) return;
434
- if (!enabled?.checked) return;
435
- if (generating) { generating = false; return; }
436
-
437
- const mainData = JSON.stringify(chart.data.datasets[0].data);
438
- const hrData = hr_chart ? JSON.stringify(hr_chart.data.datasets[0].data) : null;
439
- const needMain = mainData !== syncedData;
440
- const needHR = hrData !== null && hrData !== hr_syncedData;
441
-
442
- if (needMain || needHR) {
443
- e.preventDefault(); e.stopPropagation();
444
- await Promise.all([
445
- needMain ? (pendingSync || lib.js2py(mode,'tm', mainData).then(()=>syncedData =mainData)) : Promise.resolve(),
446
- needHR ? (hr_pendingSync || lib.js2py(mode,'tm_hr',hrData ).then(()=>hr_syncedData=hrData )) : Promise.resolve(),
447
- ]);
448
- generating = true; generate_button.click();
449
- }
450
- }, true);
451
-
452
- // ── Нижняя панель ───────────────────────────────────
453
- const PRESETS = {
454
- 'Linear (Default)': n => [{x:1,y:1},{x:n,y:n}],
455
- 'Detail Enhancer': n => [{x:1,y:1},{x:Math.max(2,Math.round(n*.25)),y:Math.round(n*.7)},{x:n,y:n}],
456
- 'Composition Lock': n => [{x:1,y:1},{x:Math.round(n*.75),y:Math.max(2,Math.round(n*.25))},{x:n,y:n}],
457
- '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}],
458
- 'Early Burst': n => [{x:1,y:1},{x:Math.round(n*.5),y:Math.round(n*.9)},{x:n,y:n}],
459
- };
460
-
461
- function makeBtn(label, title, onClick) {
462
- const b = document.createElement('button');
463
- b.textContent = label; b.type = 'button'; b.title = title || label;
464
- b.style.cssText = 'padding:3px 10px;border-radius:4px;cursor:pointer;font-size:12px';
465
- b.addEventListener('click', onClick); return b;
466
- }
467
-
468
- const bar = document.createElement('div');
469
- bar.style.cssText = 'display:flex;gap:6px;align-items:center;margin:6px 0 0;flex-wrap:wrap;font-size:13px';
470
-
471
- // ── Пресеты (built-in + custom из localStorage) ─────
472
- function _storageKeys(prefix) {
473
- const keys = [];
474
- for (let i = 0; i < localStorage.length; i++) {
475
- const k = localStorage.key(i);
476
- if (k && k.startsWith(prefix)) keys.push(k);
477
- }
478
- return keys;
479
- }
480
-
481
- function _loadCustomPresets() {
482
- const out = {};
483
- for (const k of _storageKeys('tm_presets:')) {
484
- const name = k.slice('tm_presets:'.length);
485
- try {
486
- const raw = JSON.parse(localStorage.getItem(k));
487
- let pts, savedN;
488
- if (Array.isArray(raw)) {
489
- pts = raw;
490
- savedN = Math.max(...pts.map(p => p.x));
491
- } else if (raw && Array.isArray(raw.points) && raw.points.length >= 2) {
492
- pts = raw.points;
493
- savedN = raw.savedN || Math.max(...pts.map(p => p.x));
494
- } else {
495
- continue;
496
- }
497
- if (pts.length >= 2) {
498
- out[name] = (n) => {
499
- if (n === savedN || savedN <= 1) return pts;
500
- const scale = (n - 1) / (savedN - 1);
501
- return pts.map(p => ({
502
- ...p,
503
- x: Math.max(1, Math.min(n, Math.round(1 + (p.x - 1) * scale))),
504
- }));
505
- };
506
- }
507
- } catch(e) { /* ignore */ }
508
- }
509
- return out;
510
- }
511
-
512
- function _rebuildPresetDropdown() {
513
- while (presetSel.firstChild) presetSel.removeChild(presetSel.firstChild);
514
- // Built-in
515
- Object.keys(PRESETS).forEach(n => {
516
- const o = document.createElement('option'); o.value = n; o.textContent = n;
517
- presetSel.appendChild(o);
518
- });
519
- // Custom
520
- const custom = _loadCustomPresets();
521
- const cKeys = Object.keys(custom);
522
- if (cKeys.length) {
523
- const grp = document.createElement('optgroup'); grp.label = '-- Saved --';
524
- cKeys.forEach(n => {
525
- const o = document.createElement('option'); o.value = n; o.textContent = n;
526
- grp.appendChild(o);
527
- });
528
- presetSel.appendChild(grp);
529
- }
530
- // Store custom map on the select for the apply handler
531
- presetSel._custom = custom;
532
- }
533
-
534
- const presetSel = document.createElement('select');
535
- presetSel.style.cssText = 'padding:3px 6px;border-radius:4px;cursor:pointer;flex:1;min-width:120px';
536
- _rebuildPresetDropdown();
537
-
538
- const applyBtn = makeBtn('Apply', 'Применить пресет', () => {
539
- const name = presetSel.value;
540
- const fn = PRESETS[name] || presetSel._custom[name];
541
- if (!fn) return;
542
- const n = Math.max(2, +step_ele.value);
543
- chart.data.datasets[0].data = fn(n);
544
- updateSteps(chart, n);
545
- saveState(chart); updateChartStyle(); triggerSync(chart);
546
- });
547
-
548
- const savePresetBtn = makeBtn('Save', 'Сохранить как пресет', () => {
549
- const name = prompt('Название пресета:', '');
550
- if (!name) return;
551
- try {
552
- const pts = chart.data.datasets[0].data.map(p => ({...p}));
553
- localStorage.setItem(`tm_presets:${name}`, JSON.stringify({
554
- savedN: Math.max(2, +step_ele.value),
555
- points: pts,
556
- }));
557
- } catch(e) { alert('Не удалось сохранить пресет (превышен лимит localStorage).'); return; }
558
- _rebuildPresetDropdown();
559
- presetSel.value = name;
560
- });
561
-
562
- const delPresetBtn = makeBtn('Del', 'Удалить выбранный пресет', () => {
563
- const name = presetSel.value;
564
- if (!presetSel._custom[name]) return;
565
- if (!confirm(`Удалить пресет "${name}"?`)) return;
566
- localStorage.removeItem(`tm_presets:${name}`);
567
- _rebuildPresetDropdown();
568
- });
569
-
570
- const resetBtn = makeBtn('Reset', 'Сбросить кривую к прямой', () => {
571
- const n = Math.max(2, +step_ele.value);
572
- chart.data.datasets[0].data = [{x:1,y:1},{x:n,y:n}];
573
- saveState(chart); updateChartStyle(); triggerSync(chart);
574
- });
575
-
576
- // Undo / Redo
577
- const undoBtn = makeBtn('Undo', 'Отменить (Ctrl+Z)', () => undo(chart));
578
- const redoBtn = makeBtn('Redo', 'Повторить (Ctrl+Shift+Z)', () => redo(chart));
579
-
580
- // Separator
581
- const sep = () => { const s = document.createElement('span'); s.textContent='|'; s.style.opacity='.3'; return s; };
582
-
583
- // Export
584
- const exportBtn = makeBtn('Export', 'Сохранить кривую в JSON', () => {
585
- const n = prompt('Название пресета:', 'my_curve') || 'my_curve';
586
- const data = {
587
- name: n, version: 1,
588
- points: chart.data.datasets[0].data,
589
- interpolation: getInterpMode(),
590
- };
591
- const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
592
- const url = URL.createObjectURL(blob);
593
- const a = document.createElement('a');
594
- a.href = url; a.download = `${n.replace(/\s+/g,'_')}.json`; a.click();
595
- URL.revokeObjectURL(url);
596
- });
597
-
598
- // Import
599
- const importBtn = makeBtn('Import', 'Загрузить кривую из JSON', () => {
600
- const inp = document.createElement('input');
601
- inp.type = 'file'; inp.accept = '.json';
602
- inp.addEventListener('change', () => {
603
- const file = inp.files[0]; if (!file) return;
604
- const reader = new FileReader();
605
- reader.onload = ev => {
606
- try {
607
- const d = JSON.parse(ev.target.result);
608
- if (!Array.isArray(d.points)) return;
609
- const n = Math.max(2, +step_ele.value);
610
- const pts = d.points.filter(p => p.x>=1&&p.x<=n&&p.y>=1&&p.y<=n);
611
- chart.data.datasets[0].data = pts;
612
- updateSteps(chart, n);
613
- saveState(chart); triggerSync(chart);
614
- } catch(err) { console.error('[TimeMachine] Import error:', err); }
615
- };
616
- reader.readAsText(file);
617
- });
618
- inp.click();
619
- });
620
-
621
- // Show sigma
622
- const sigmaBtn = makeBtn('Sigma', 'Показать σ-значения на оси Y (реальные через HTTP, fallback Karras)', () => {
623
- showSigma = !showSigma;
624
- sigmaBtn.textContent = showSigma ? 'Step' : 'Sigma';
625
- chart.update(); if (hr_chart) hr_chart.update();
626
- });
627
-
628
- // ── Panel system ────────────────────────────────────────────────
629
- let _panelDiv = null;
630
- function _closePanel() { if (_panelDiv) { _panelDiv.remove(); _panelDiv = null; } }
631
-
632
- function _openPanel(title, buildFn) {
633
- _closePanel();
634
- const d = document.createElement('div');
635
- d.id = id('panel');
636
- 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';
637
- const h = document.createElement('div');
638
- h.style.cssText = 'font-weight:bold;margin-bottom:6px';
639
- h.textContent = title;
640
- d.appendChild(h);
641
- buildFn(d);
642
- $('#' + id('container')).appendChild(d);
643
- _panelDiv = d;
644
- }
645
-
646
- // ── Gen… (procedural curves) ────────────────────────────────────
647
- const genBtn = makeBtn('Gen…', 'Сгенерировать кривую (синус / случайная)', () => {
648
- let prevData = chart.data.datasets[0].data.slice();
649
- function getN() { return Math.max(2, +step_ele.value); }
650
-
651
- function _sine(freq, amp) {
652
- const n = getN();
653
- const pts = [{x:1,y:1}];
654
- for (let x = 2; x < n; x++) {
655
- const t = (x-1)/(n-1);
656
- const mid = 1 + (n-1)*t;
657
- const y = Math.round(mid + amp*(n-1)*Math.sin(2*Math.PI*freq*t));
658
- pts.push({x, y: Math.max(1, Math.min(n, y))});
659
- }
660
- pts.push({x:n, y:n});
661
- return pts;
662
- }
663
-
664
- function _walk(stepSz, seed) {
665
- const n = getN();
666
- let rng = seed || Date.now();
667
- function rand() { rng ^= rng<<13; rng ^= rng>>17; rng ^= rng<<5; return (rng>>>0)/4294967296; }
668
- const pts = [{x:1,y:1}];
669
- let y = 1;
670
- for (let x = 2; x < n; x++) {
671
- y += (rand()-0.5)*2*stepSz;
672
- y = Math.max(1, Math.min(n, Math.round(y)));
673
- pts.push({x, y});
674
- }
675
- pts.push({x:n, y:n});
676
- return pts;
677
- }
678
-
679
- _openPanel('Generate Curve', panel => {
680
- const row1 = document.createElement('div');
681
- row1.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px';
682
- const sel = document.createElement('select');
683
- sel.innerHTML = '<option>Sine</option><option>Random walk</option>';
684
-
685
- const p1Label = document.createElement('span');
686
- p1Label.style.cssText = 'width:55px;text-align:right';
687
- const p1 = document.createElement('input');
688
- p1.type = 'range'; p1.min = 0.5; p1.max = 10; p1.step = 0.5; p1.value = 2;
689
-
690
- const p2Label = document.createElement('span');
691
- p2Label.style.cssText = 'width:55px;text-align:right';
692
- const p2 = document.createElement('input');
693
- p2.type = 'range'; p2.min = 0; p2.max = 1; p2.step = 0.05; p2.value = 0.3;
694
-
695
- function updateLabels() {
696
- if (sel.value === 'Sine') {
697
- p1Label.textContent = `Freq: ${p1.value}`;
698
- p1.min = 0.5; p1.max = 10; p1.step = 0.5;
699
- p2Label.textContent = `Amp: ${p2.value}`;
700
- p2.min = 0; p2.max = 1; p2.step = 0.05;
701
- } else {
702
- p1Label.textContent = `Step: ${p1.value}`;
703
- p1.min = 0.1; p1.max = 5; p1.step = 0.1;
704
- p2Label.textContent = `Seed: ${p2.value}`;
705
- p2.min = 0; p2.max = 9999; p2.step = 1;
706
- }
707
- }
708
-
709
- function preview() {
710
- const n = getN();
711
- const pts = sel.value === 'Sine' ? _sine(+p1.value, +p2.value) : _walk(+p1.value, +p2.value);
712
- chart.data.datasets[0].data = pts;
713
- updateSteps(chart, n);
714
- chart.update();
715
- }
716
-
717
- sel.addEventListener('change', () => { updateLabels(); preview(); });
718
- p1.addEventListener('input', () => { updateLabels(); preview(); });
719
- p2.addEventListener('input', () => { updateLabels(); preview(); });
720
- updateLabels(); preview();
721
-
722
- const apply = makeBtn('Apply', '', () => {
723
- saveState(chart); updateChartStyle(); triggerSync(chart);
724
- _closePanel();
725
- });
726
- const cancel = makeBtn('Cancel', '', () => {
727
- const n = getN();
728
- chart.data.datasets[0].data = prevData;
729
- updateSteps(chart, n); chart.update();
730
- _closePanel();
731
- });
732
- row1.append(sel, p1Label, p1, p2Label, p2, apply, cancel);
733
- panel.appendChild(row1);
734
- });
735
- });
736
-
737
- // ── Rasterize control points → dense array of length n ───────
738
- const _WEIGHT = {
739
- 'Ease In': t => t * t,
740
- 'Ease Out': t => 1 - (1-t)*(1-t),
741
- 'Ease In-Out': t => t*t*(3-2*t),
742
- 'Cubic': t => t*t*t,
743
- 'Exponential': t => Math.pow(2,t)-1,
744
- 'Step': t => 0,
745
- };
746
-
747
- function _rasterize(pts, n) {
748
- const sorted = [...pts].sort((a,b)=>a.x-b.x);
749
- const out = [];
750
- let si = 0;
751
- for (let x = 1; x <= n; x++) {
752
- while (si < sorted.length-2 && sorted[si+1].x <= x) si++;
753
- const p0 = sorted[si], p1 = sorted[Math.min(si+1, sorted.length-1)];
754
- let y;
755
- if (p0.x === p1.x) { y = p0.y; }
756
- else {
757
- const mode = p0.mode || '';
758
- const t = (x-p0.x)/(p1.x-p0.x);
759
- if (mode === 'Step') { y = p0.y; }
760
- else {
761
- const w = _WEIGHT[mode] || (t => t);
762
- y = p0.y + w(t) * (p1.y - p0.y);
763
- }
764
- }
765
- out.push({x, y: Math.max(1, Math.min(n, Math.round(y)))});
766
- }
767
- return out;
768
- }
769
-
770
- // ── Blend… ──────────────────────────────────────────────────────
771
- const blendBtn = makeBtn('Blend…', 'Смешать два пресета', () => {
772
- const n = Math.max(2, +step_ele.value);
773
- let prevData = chart.data.datasets[0].data.slice();
774
- const allPresets = {...PRESETS, ...presetSel._custom};
775
- const names = Object.keys(allPresets);
776
-
777
- _openPanel('Blend Presets', panel => {
778
- const row = document.createElement('div');
779
- row.style.cssText = 'display:flex;gap:6px;align-items:center;flex-wrap:wrap';
780
-
781
- const sA = document.createElement('select');
782
- const sB = document.createElement('select');
783
- names.forEach(name => { sA.innerHTML += `<option>${name}</option>`; sB.innerHTML += `<option>${name}</option>`; });
784
- if (names.length > 1) sB.selectedIndex = 1;
785
-
786
- const sl = document.createElement('input');
787
- sl.type = 'range'; sl.min = 0; sl.max = 100; sl.value = 50;
788
- const slLabel = document.createElement('span');
789
- slLabel.style.cssText = 'width:40px';
790
-
791
- function _getPresetFn(name) { return PRESETS[name] || presetSel._custom[name]; }
792
-
793
- function _blend() {
794
- const fnA = _getPresetFn(sA.value);
795
- const fnB = _getPresetFn(sB.value);
796
- if (!fnA || !fnB) return null;
797
- const cA = fnA(n), cB = fnB(n);
798
- const denseA = _rasterize(cA, n);
799
- const denseB = _rasterize(cB, n);
800
- const t = +sl.value / 100;
801
- slLabel.textContent = `${sl.value}%`;
802
- return denseA.map((pt, i) => ({
803
- x: pt.x,
804
- y: Math.round(pt.y*(1-t) + denseB[i].y*t),
805
- }));
806
- }
807
-
808
- function preview() { const pts = _blend(); if (pts) { chart.data.datasets[0].data = pts; updateSteps(chart, n); chart.update(); } }
809
-
810
- sA.addEventListener('change', preview);
811
- sB.addEventListener('change', preview);
812
- sl.addEventListener('input', preview);
813
- preview();
814
-
815
- const apply = makeBtn('Apply', '', () => { saveState(chart); updateChartStyle(); triggerSync(chart); _closePanel(); });
816
- const cancel = makeBtn('Cancel', '', () => { chart.data.datasets[0].data = prevData; updateSteps(chart, n); chart.update(); _closePanel(); });
817
-
818
- row.append(sA, sB, sl, slLabel, apply, cancel);
819
- panel.appendChild(row);
820
- });
821
- });
822
-
823
- bar.append(presetSel, applyBtn, savePresetBtn, delPresetBtn, resetBtn, sep(), undoBtn, redoBtn, sep(), exportBtn, importBtn, sep(), sigmaBtn, genBtn, blendBtn);
824
- $('#' + id('container')).appendChild(bar);
825
-
826
- // ── One-shot: автосброс Enabled после генерации ────────────────
827
- // Паттерн из script.js A1111: следим за interruptBtn.style.display
828
- if (oneShotEl) {
829
- const interruptBtn = gradioApp().querySelector(`#${mode}_interrupt`);
830
- if (interruptBtn) {
831
- let wasGen = false;
832
- new MutationObserver(() => {
833
- const isGen = interruptBtn.style.display === 'block';
834
- if (isGen && !wasGen) {
835
- wasGen = true;
836
- } else if (!isGen && wasGen) {
837
- wasGen = false;
838
- if (oneShotEl.checked && enabled?.checked) {
839
- enabled.checked = false;
840
- enabled.dispatchEvent(new Event('change', { bubbles: true }));
841
- }
842
- }
843
- }).observe(interruptBtn, { attributes: true, attributeFilter: ['style'] });
844
- }
845
- }
846
- }
847
-
848
- // ── Общие функции ────────────────────────────────────────────────
849
-
850
- function createInitialData() {
851
- return { datasets: [{
852
- type:'line', showLine:true, tension:0,
853
- backgroundColor:'rgba(255,140,0,.6)', borderColor:'rgba(255,140,0,.6)',
854
- borderWidth:2, borderCapStyle:'round', borderJoinStyle:'round',
855
- borderDash:[], borderDashOffset:0,
856
- pointBorderColor:'rgba(255,140,0,.6)', pointBackgroundColor:'rgba(255,140,0,.6)',
857
- pointBorderWidth:1, pointHoverBorderWidth:10, pointRadius:5, pointHitRadius:10,
858
- fill:false, data:[],
859
- }]};
860
- }
861
-
862
- function createChartOption() {
863
- return {
864
- responsive:false,
865
- events:['mouseup','mousedown','mousemove','mouseout','click','touchstart','touchmove'],
866
- scales:{
867
- x:{type:'linear',display:true,title:{display:true,text:'timesteps'},ticks:{major:{enabled:true}},min:0,max:100,stepSize:10},
868
- y:{type:'linear',display:true,title:{display:true,text:'actual timesteps'},ticks:{major:{enabled:true}},min:0,max:100,stepSize:10},
869
- },
870
- chartArea:{backgroundColor:'rgba(255,255,255,1)'},
871
- animation:{duration:100},
872
- plugins:{legend:{display:false},tooltip:{callbacks:{
873
- title:ctxs=>ctxs.map(c=>`${c.parsed.x} → ${c.parsed.y}`), label:()=>'',
874
- }}},
875
- };
876
- }
877
-
878
- function createPlugins(canvas, step_ele, cutoffEl) {
879
- const plugins = [{
880
- id:'dragpoint',
881
- beforeEvent(chart, args) {
882
- const t = args.event.type;
883
- if (t === 'mousedown') {
884
- const btn = args.event.native.button;
885
- if (btn===0) this.startDrag(chart, args);
886
- else if (btn===2) this.removePoint(chart, args);
887
- } else if (t==='mouseup'||t==='mouseout') {
888
- if (this.dragctx.item) this.endDrag(chart, args);
889
- } else if (t==='mousemove') {
890
- if (this.dragctx.item) this.onDrag(chart, args);
891
- }
892
- },
893
- afterDraw(chart) {
894
- const ctx = chart.ctx; ctx.save();
895
- // Cutoff line
896
- const cs = cutoffEl ? +cutoffEl.value : 0;
897
- if (cs > 0) {
898
- const xa = chart.scales.x, ya = chart.scales.y, x = xa.getPixelForValue(cs);
899
- ctx.beginPath(); ctx.setLineDash([6, 4]); ctx.strokeStyle = 'rgba(220,50,50,0.6)';
900
- ctx.lineWidth = 2; ctx.moveTo(x, ya.top); ctx.lineTo(x, ya.bottom); ctx.stroke();
901
- }
902
- // Per-segment mode indicators
903
- const data = chart.data.datasets[0].data;
904
- for (let i = 0; i < data.length - 1; i++) {
905
- const mode = data[i].mode;
906
- if (!mode) continue;
907
- const x0 = chart.scales.x.getPixelForValue(data[i].x);
908
- const y0 = chart.scales.y.getPixelForValue(data[i].y);
909
- const x1 = chart.scales.x.getPixelForValue(data[i+1].x);
910
- const y1 = chart.scales.y.getPixelForValue(data[i+1].y);
911
- const mx = (x0+x1)/2, my = (y0+y1)/2;
912
- ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2*Math.PI);
913
- ctx.fillStyle = '#ff8c00'; ctx.fill();
914
- ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
915
- ctx.fillStyle = '#fff';
916
- ctx.font = 'bold 8px sans-serif';
917
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
918
- ctx.fillText(mode[0], mx, my);
919
- }
920
- ctx.restore();
921
- },
922
- dragctx:{item:null, max_steps:()=>Math.max(1,+step_ele.value)},
923
- lastRemoved:false,
924
- startDrag(chart,args){const item=this.getItem(chart,args);if(item)this.dragctx.item=item;else this.addPoint(chart,args);},
925
- 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();}},
926
- endDrag(chart){this.dragctx.item=null;},
927
- 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});},
928
- 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();}}},
929
- 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()));},
930
- 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()));},
931
- getItem(chart,args){const items=chart.getElementsAtEventForMode(args.event,'nearest',{intersect:true},false);return items.length===0?null:items[0];},
932
- 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();},
933
- }];
934
-
935
- return plugins;
936
- }
937
-
938
- function updateSteps(chart, max_steps) {
939
- max_steps = Math.max(2, max_steps);
940
- chart.options.scales.x.max = max_steps+1;
941
- chart.options.scales.y.max = max_steps+1;
942
- const data = chart.data.datasets[0].data;
943
- const new_data = [];
944
- for (const pt of data) if(1<=pt.x&&pt.x<=max_steps) new_data.push({...pt, y:Math.min(pt.y,max_steps)});
945
- if (!new_data.find(c=>c.x===1)) new_data.unshift({x:1, y:1});
946
- if (!new_data.find(c=>c.x===max_steps)) new_data.push ({x:max_steps,y:max_steps});
947
- chart.data.datasets[0].data = new_data;
948
- chart.update();
949
- }
950
-
951
- })('TimeMachine');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sd-webui-timemachine-fixed/scripts/timemachine.py DELETED
@@ -1,372 +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.sampler_name)
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 _on_app_started(demo, app):
361
- @app.get("/timemachine/sigmas")
362
- def get_sigmas(scheduler: str = 'karras', steps: int = 20, sampler: str | None = None):
363
- try:
364
- if scheduler == 'Use sampler default' and sampler:
365
- scheduler = sampler_utils.get_default_scheduler_for(sampler)
366
- sigmas = sampler_utils.compute_sigmas(steps, scheduler)
367
- return {"sigmas": sigmas.tolist()}
368
- except Exception as e:
369
- print(f'[TimeMachine] sigmas API error: {e}')
370
- return JSONResponse(content={"sigmas": []}, status_code=500)
371
-
372
- 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,293 +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(sampler_name: str) -> str:
114
- """Возвращает scheduler по умолчанию для данного семплера."""
115
- from modules import sd_samplers
116
- config = sd_samplers.find_sampler_config(sampler_name)
117
- if config:
118
- return config.options.get('scheduler', 'karras')
119
- return 'karras'
120
-
121
-
122
- def get_scheduler_choices() -> list[str]:
123
- """Список планировщиков для UI-дропдауна."""
124
- skip = {'automatic', 'Automatic'}
125
- return ['Use sampler default'] + [
126
- s.label
127
- for s in sd_schedulers.schedulers
128
- if s.name not in skip and s.label not in skip and s.function is not None
129
- ]
130
-
131
-
132
- UNSUPPORTED_SAMPLERS = {'DPM fast', 'DPM adaptive'}
133
-
134
-
135
- # ═══════════════════════════════════════════════════════════════
136
- # Noise Injection (NI) семплеры
137
- #
138
- # Физическая идея: когда σ растёт между шагами (Time Travel),
139
- # латент находится «слишком чистым» для текущего уровня шума.
140
- # Правильное решение — добавить шум: x += √(σ²_{k+1} − σ²_k) · ε
141
- #
142
- # Кастомные циклы обязательны: стандартные сэмплеры k-diffusion
143
- # не знают о noise injection, и обёртка модели не может обновить
144
- # x во внешней функции (см. NIModelWrapper — удалена как ошибочная).
145
- # ═══════════════════════════════════════════════════════════════
146
-
147
- import tqdm as _tqdm
148
- from modules import sd_samplers, sd_samplers_common
149
- import modules.sd_samplers_kdiffusion as K
150
-
151
-
152
- class TimeMachineEulerNI:
153
- """
154
- Euler + Noise Injection. Кастомный цикл — NI модифицирует x
155
- напрямую, затем вычисляется стандартный Euler-шаг.
156
- """
157
- @torch.no_grad()
158
- def __call__(self, model, x, sigmas, extra_args=None, callback=None, disable=None,
159
- s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1., **kwargs):
160
- extra_args = extra_args or {}
161
- s_in = x.new_ones([x.shape[0]])
162
- last_sigma: float | None = None
163
- n_steps = len(sigmas) - 1
164
-
165
- for i in _tqdm.tqdm(range(n_steps), disable=disable):
166
- sigma_curr = sigmas[i]
167
- sigma_next = sigmas[i + 1]
168
-
169
- # ── Noise Injection (σ выросла → добавить шум) ──────────
170
- sv = sigma_curr.item()
171
- if last_sigma is not None and sv > last_sigma:
172
- delta_var = sv ** 2 - last_sigma ** 2
173
- if delta_var > 0:
174
- x = x + torch.randn_like(x) * math.sqrt(delta_var)
175
- last_sigma = sv
176
-
177
- # ── s_churn (ε-буст для exploration) ─────────────────────
178
- gamma = min(s_churn / n_steps, math.sqrt(2) - 1) if s_tmin <= sv <= s_tmax else 0.
179
- sigma_hat = sigma_curr * (gamma + 1)
180
- if gamma > 0:
181
- eps = torch.randn_like(x) * s_noise
182
- x = x + eps * math.sqrt(sigma_hat ** 2 - sv ** 2)
183
-
184
- # ── Euler шаг ────────────────────────────────────────────
185
- denoised = model(x, sigma_hat * s_in, **extra_args)
186
- if callback is not None:
187
- callback({'x': x, 'i': i, 'sigma': sigma_hat,
188
- 'sigma_hat': sigma_hat, 'denoised': denoised})
189
-
190
- d = (x - denoised) / sigma_hat
191
- dt = sigma_next - sigma_hat
192
- x = x + d * dt
193
-
194
- return x
195
-
196
-
197
- class TimeMachineDPM2MNI:
198
- """
199
- DPM++ 2M + Noise Injection.
200
-
201
- Кастомный цикл: при NI кеш old_denoised сбрасывается,
202
- иначе второй порядок использует значение с неверного состояния.
203
- """
204
- @torch.no_grad()
205
- def __call__(self, model, x, sigmas, extra_args=None, callback=None, disable=None, **kwargs):
206
- extra_args = extra_args or {}
207
- s_in = x.new_ones([x.shape[0]])
208
- sigma_fn = lambda t: t.neg().exp()
209
- t_fn = lambda s: s.log().neg()
210
- old_denoised = None
211
- last_sigma: float | None = None
212
-
213
- for i in _tqdm.tqdm(range(len(sigmas) - 1), disable=disable):
214
- sigma_curr = sigmas[i]
215
- sigma_next = sigmas[i + 1]
216
-
217
- # ── Noise Injection ──────────────────────────────────────
218
- sv = sigma_curr.item()
219
- if last_sigma is not None and sv > last_sigma:
220
- delta_var = sv ** 2 - last_sigma ** 2
221
- if delta_var > 0:
222
- x = x + torch.randn_like(x) * math.sqrt(delta_var)
223
- old_denoised = None # кеш невалиден после re-noise
224
- last_sigma = sv
225
-
226
- # ── DPM++ 2M шаг ───────────���─────────────────────────────
227
- denoised = model(x, sigma_curr * s_in, **extra_args)
228
- if callback is not None:
229
- callback({'x': x, 'i': i, 'sigma': sigma_curr,
230
- 'sigma_hat': sigma_curr, 'denoised': denoised})
231
-
232
- t, t_next = t_fn(sigma_curr), t_fn(sigma_next)
233
- h = t_next - t
234
-
235
- if old_denoised is None or sigma_next == 0 or h == 0:
236
- # Euler: первый шаг, после NI-сброса, последний шаг,
237
- # или flat sigma (h=0 — деление на ноль в 2M формулах)
238
- x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised
239
- else:
240
- # DPM++ 2M второй порядок
241
- h_last = t - t_fn(sigmas[i - 1])
242
- r = h_last / h
243
- denoised_d = (1 + 1 / (2 * r)) * denoised - (1 / (2 * r)) * old_denoised
244
- x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_d
245
-
246
- old_denoised = denoised
247
-
248
- return x
249
-
250
-
251
- # ─────────────────────────────────────────────────────────────────
252
- # Регистрация NI семплеров в A1111 с поддержкой s_churn
253
- # ─────────────────────────────────────────────────────────────────
254
-
255
- class _TMSampler(K.KDiffusionSampler):
256
- """
257
- Обёртка над KDiffusionSampler, которая форсирует extra_params
258
- для s_churn/s_tmin/s_tmax/s_noise (родной A1111 маппинг
259
- sampler_extra_params не работает для TM-сэмплеров, т.к.
260
- ключуется по строковому имени функции, а у нас — объект).
261
- """
262
- def __init__(self, func, sd_model, options=None):
263
- super().__init__(func, sd_model, options)
264
- self.extra_params = ['s_churn', 's_tmin', 's_tmax', 's_noise']
265
-
266
-
267
- def register_ni_samplers() -> None:
268
- """
269
- Добавляет 'TM Euler (NI)' и 'TM DPM++ 2M (NI)' в список семплеров A1111.
270
- Используются совместно с TimeMachine: sigma override задаёт расписание,
271
- NI добавляет физически правильный шум при backward-шагах.
272
- Без TimeMachine ведут себя идентично Euler / DPM++ 2M (NI не срабатывает).
273
- """
274
- ni_entries = [
275
- ('TM Euler (NI)', TimeMachineEulerNI(), ['k_tm_euler_ni'], {}),
276
- ('TM DPM++ 2M (NI)', TimeMachineDPM2MNI(), ['k_tm_dpmpp2m_ni'], {'scheduler': 'karras'}),
277
- ]
278
- added = False
279
- for label, func, aliases, options in ni_entries:
280
- if label not in [x.name for x in sd_samplers.all_samplers]:
281
- # default-аргументы f= и o= фиксируют текущие значения в closure
282
- data = sd_samplers_common.SamplerData(
283
- label,
284
- lambda model, f=func, o=options: _TMSampler(f, model, options=o),
285
- aliases,
286
- options,
287
- )
288
- sd_samplers.all_samplers.append(data)
289
- added = True
290
-
291
- if added:
292
- sd_samplers.set_samplers()
293
- 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)