diff --git a/app/ui/features/training/index.js b/app/ui/features/training/index.js index 5d5bab66..60eab916 100644 --- a/app/ui/features/training/index.js +++ b/app/ui/features/training/index.js @@ -114,6 +114,7 @@ export default { await session.flush(); const settings = settingsOf(doc); delete settings.preset; // a starting point the person picked, not a setting of the trainer + delete settings.preview_strength; // used by the previews only settings.trigger = doc.trigger || ""; settings.postfix = doc.postfix || ""; if (resumeId) settings.resume = resumeId; @@ -197,6 +198,7 @@ export default { refreshLive(); } /** A picture or clip made by the pipeline with the LoRA so far; when `auto`, training carries on afterwards. */ + const previewStrength = () => { const v = Number(settingsOf(session.doc).preview_strength); return Number.isFinite(v) ? v : 1; }; async function previewNow(auto) { const paused = ui.run.paused; if (!paused || !paused.preview) return; @@ -204,7 +206,7 @@ export default { setRun({ state: "previewing", message: `Making a preview with the LoRA at step ${paused.step}` }); draw(); let out; - try { out = app.runner && app.runner.preview ? await app.runner.preview({ lora: paused.preview }) : { error: "Previews need the app's Generate." }; } catch (err) { out = { error: err.message }; } + try { out = app.runner && app.runner.preview ? await app.runner.preview({ lora: paused.preview, strength: previewStrength() }) : { error: "Previews need the app's Generate." }; } catch (err) { out = { error: err.message }; } if (out.image) { const im = out.image; ui.previews.push({ id: `p${ui.previews.length + 1}`, step: paused.step, src: { kind: "output", type: im.type || "output", subfolder: im.subfolder || "", filename: im.filename }, media_kind: VIDEO.test(im.filename) ? "video" : "image" }); diff --git a/app/ui/features/training/train.js b/app/ui/features/training/train.js index 852b3fe3..cca44395 100644 --- a/app/ui/features/training/train.js +++ b/app/ui/features/training/train.js @@ -4,7 +4,7 @@ import { stillOf } from "./state.js"; /** The node's own defaults. A set remembers what was changed from them. */ export const DEFAULTS = { - name: "funpack_lora", use: "all", preview_every: 0, save_every: 0, steps: 500, learning_rate: 0.0001, rank: 16, algorithm: "LoRA", optimizer: "AdamW", loss: "MSE", + name: "funpack_lora", use: "all", preview_every: 0, preview_strength: 1, save_every: 0, steps: 500, learning_rate: 0.0001, rank: 16, algorithm: "LoRA", optimizer: "AdamW", loss: "MSE", batch_size: 1, grad_accumulation: 1, resolution: 512, aspect_buckets: false, frames: 33, fps: 0, start: "start", seed: 0, training_dtype: "bf16", lora_dtype: "bf16", gradient_checkpointing: true, offloading: false, bypass: false, swap_blocks: 0, }; @@ -94,6 +94,7 @@ export function trainStep({ app, session, go, ui }) { num("learning_rate", "Learning rate", "How big each step is. Lower is safer and slower.", { min: 0.0000001, max: 1, step: 0.0001, precision: 7 }), num("rank", "Rank", "How much it can learn. Higher holds more detail and makes a bigger file.", { min: 1, max: 128, step: 1 }), num("save_every", "Save a copy every (steps)", "0 saves only the finished LoRA. Otherwise a copy of the LoRA so far goes to your loras folder every this many steps, named like yourname_step200, so you can pick the best moment.", { min: 0, max: 100000, step: 10 }), + num("preview_strength", "Preview strength", "How strongly the LoRA is applied in previews. 1 is the usual; use a negative value (for example -1) to see a LoRA trained to be subtracted. Only for previews: it is not part of the training.", { min: -10, max: 10, step: 0.1, precision: 2 }), num("preview_every", "Preview every (steps)", "0 never. Otherwise training waits every this many steps while your pipeline makes a picture or clip with the LoRA so far (the picked scene, the project's own settings), then carries on. Needs a LoRA loader in the pipeline; each wait loads the models again.", { min: 0, max: 100000, step: 10 })] }), c.group.default({ label: "Pictures and clips", rows: [ num("resolution", "Size", "Pixels per side. Everything is fitted to about this area.", { min: 64, max: 2048, step: 32 }), diff --git a/app/ui/features/training/training.test.js b/app/ui/features/training/training.test.js index b110cb20..8dba010d 100644 --- a/app/ui/features/training/training.test.js +++ b/app/ui/features/training/training.test.js @@ -296,6 +296,7 @@ test("Tags step: a clip's frame count and rate show beside its label", async () test("a preview step pauses the run, the pipeline makes a preview with the LoRA so far, and training carries on", async () => { const f = fake(); + f.doc.settings = { preview_strength: -0.8 }; const previews = []; f.app.runner = { preview: async (o) => { previews.push(o); return { image: { filename: "pv.mp4", subfolder: "", type: "output" } }; } }; let queued = 0, polls = 0; @@ -318,6 +319,8 @@ test("a preview step pauses the run, the pipeline makes a preview with the LoRA await wait(3600); assert.equal(previews.length, 1); assert.equal(previews[0].lora, "cats_preview.safetensors"); + assert.equal(previews[0].strength, -0.8, "the preview uses the strength set for it, here a negative one"); + assert.ok(!("preview_strength" in f.calls.find((c) => c[0] === "plan")[2]), "and it is not sent to the trainer"); assert.equal(queued, 2, "training was queued again to carry on"); const plans = f.calls.filter((c) => c[0] === "plan"); assert.equal(plans[1][2].resume, "r1");