From b99c76857bda579da1b1ba98fb311b8f77f73d12 Mon Sep 17 00:00:00 2001 From: modelflat Date: Sat, 9 Nov 2024 00:15:37 +0100 Subject: [PATCH 1/4] Add a grace period between detecting a change and triggering generation in live preview * This will prevent some of the "useless" generations, e.g. from the very start of the brush stroke * Period is configurable in settings; setting the default to 0 to preserve the existing behaviour This at least partially addresses/follows the discussions in #628 and #1248 --- ai_diffusion/model.py | 28 ++++++++++++++++++++-------- ai_diffusion/settings.py | 7 +++++++ ai_diffusion/ui/settings.py | 4 ++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 9aa592e1d..2963b1db8 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -296,7 +296,7 @@ def estimate_cost(self, kind=JobKind.diffusion): def generate_live(self): eventloop.run(_report_errors(self, self._generate_live())) - async def _generate_live(self, last_input: WorkflowInput | None = None): + def _prepare_live_job_params(self): strength = self.live.strength workflow_kind = WorkflowKind.generate if strength == 1.0 else WorkflowKind.refine client = self._connection.client @@ -344,12 +344,15 @@ async def _generate_live(self, last_input: WorkflowInput | None = None): inpaint=inpaint if mask else None, is_live=True, ) + params = JobParams(bounds, conditioning.positive, regions=job_regions) + return input, params + + async def _generate_live(self, last_input: WorkflowInput | None = None): + input, job_params = self._prepare_live_job_params() if input != last_input: self.clear_error() - params = JobParams(bounds, conditioning.positive, regions=job_regions) - await self.enqueue_jobs(input, JobKind.live_preview, params) + await self.enqueue_jobs(input, JobKind.live_preview, job_params) return input - return None async def _generate_custom(self, previous_input: WorkflowInput | None): @@ -883,12 +886,21 @@ def handle_job_finished(self, job: Job): eventloop.run(_report_errors(self._model, self._continue_generating())) async def _continue_generating(self): + just_got_here = True while self.is_active and self._model.document.is_active: - new_input = await self._model._generate_live(self._last_input) - if new_input is not None: # frame was scheduled - self._last_input = new_input - return + new_input, _ = self._model._prepare_live_job_params() + if self._last_input != new_input: + if settings.live_redraw_grace_period > 0 and not just_got_here: + # only use grace period if this isn't our first frame of polling + # if it is, and there are changes in the input, it's likely that we have some changes we ignored + # previously due to the generation process running, and we need to update the preview asap + await asyncio.sleep(settings.live_redraw_grace_period) + new_input = await self._model._generate_live(self._last_input) + if new_input is not None: + self._last_input = new_input + return # no changes in input data + just_got_here = False await asyncio.sleep(self._poll_rate) def apply_result(self, layer_only=False): diff --git a/ai_diffusion/settings.py b/ai_diffusion/settings.py index e5fb466b8..c834d5e70 100644 --- a/ai_diffusion/settings.py +++ b/ai_diffusion/settings.py @@ -155,6 +155,13 @@ class Settings(QObject): _("Pick a new seed after copying the result to the canvas in Live mode"), ) + live_redraw_grace_period: float + _live_redraw_grace_period = Setting( + _("Live: Redraw grace period"), + 0.0, + _("How long to delay scheduling the live preview job for after a change is made"), + ) + prompt_translation: str _prompt_translation = Setting( _("Prompt Translation"), diff --git a/ai_diffusion/ui/settings.py b/ai_diffusion/ui/settings.py index 7e48ed697..796f701c0 100644 --- a/ai_diffusion/ui/settings.py +++ b/ai_diffusion/ui/settings.py @@ -439,6 +439,10 @@ def __init__(self): self.add("auto_preview", SwitchSetting(S._auto_preview, parent=self)) self.add("show_steps", SwitchSetting(S._show_steps, parent=self)) self.add("new_seed_after_apply", SwitchSetting(S._new_seed_after_apply, parent=self)) + self.add( + "live_redraw_grace_period", + SliderSetting(S._live_redraw_grace_period, self, 0.0, 3.0, "{} s"), + ) self.add("debug_dump_workflow", SwitchSetting(S._debug_dump_workflow, parent=self)) languages = [(lang.name, lang.id) for lang in Localization.available] From 7b9f98fe0ac45335561f3ff995f9139eb55610c5 Mon Sep 17 00:00:00 2001 From: modelflat Date: Tue, 26 Nov 2024 12:46:09 +0100 Subject: [PATCH 2/4] Refactor the generation loop --- ai_diffusion/model.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 2963b1db8..dc72770fc 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -4,6 +4,7 @@ from dataclasses import replace from pathlib import Path from enum import Enum +import time from typing import Any, NamedTuple from PyQt5.QtCore import QObject, QUuid, pyqtSignal, Qt from PyQt5.QtGui import QImage, QPainter, QColor, QBrush @@ -294,9 +295,10 @@ def estimate_cost(self, kind=JobKind.diffusion): return 0 def generate_live(self): - eventloop.run(_report_errors(self, self._generate_live())) + input, job_params = self._prepare_live_workflow() + eventloop.run(_report_errors(self, self._generate_live(input, job_params))) - def _prepare_live_job_params(self): + def _prepare_live_workflow(self): strength = self.live.strength workflow_kind = WorkflowKind.generate if strength == 1.0 else WorkflowKind.refine client = self._connection.client @@ -347,13 +349,9 @@ def _prepare_live_job_params(self): params = JobParams(bounds, conditioning.positive, regions=job_regions) return input, params - async def _generate_live(self, last_input: WorkflowInput | None = None): - input, job_params = self._prepare_live_job_params() - if input != last_input: - self.clear_error() - await self.enqueue_jobs(input, JobKind.live_preview, job_params) - return input - return None + async def _generate_live(self, input: WorkflowInput, job_params: JobParams): + self.clear_error() + await self.enqueue_jobs(input, JobKind.live_preview, job_params) async def _generate_custom(self, previous_input: WorkflowInput | None): if self.workspace is not Workspace.custom or not self.document.is_active: @@ -840,6 +838,7 @@ class LiveWorkspace(QObject, ObservableProperties): _model: Model _last_input: WorkflowInput | None = None + _last_change: float = 0 _result: Image | None = None _result_composition: Image | None = None _result_params: JobParams | None = None @@ -886,21 +885,16 @@ def handle_job_finished(self, job: Job): eventloop.run(_report_errors(self._model, self._continue_generating())) async def _continue_generating(self): - just_got_here = True while self.is_active and self._model.document.is_active: - new_input, _ = self._model._prepare_live_job_params() + new_input, job_params = self._model._prepare_live_workflow() if self._last_input != new_input: - if settings.live_redraw_grace_period > 0 and not just_got_here: - # only use grace period if this isn't our first frame of polling - # if it is, and there are changes in the input, it's likely that we have some changes we ignored - # previously due to the generation process running, and we need to update the preview asap - await asyncio.sleep(settings.live_redraw_grace_period) - new_input = await self._model._generate_live(self._last_input) - if new_input is not None: + now = time.monotonic() + if self._last_change + settings.live_redraw_grace_period <= now: + await self._model._generate_live(new_input, job_params) self._last_input = new_input return - # no changes in input data - just_got_here = False + else: + self._last_change = time.monotonic() await asyncio.sleep(self._poll_rate) def apply_result(self, layer_only=False): From 80b2bc4c5598f4e95295ee042645672d3975e13c Mon Sep 17 00:00:00 2001 From: modelflat Date: Tue, 26 Nov 2024 12:46:56 +0100 Subject: [PATCH 3/4] Remove unused imports --- ai_diffusion/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index dc72770fc..294d40b2d 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -5,9 +5,9 @@ from pathlib import Path from enum import Enum import time -from typing import Any, NamedTuple +from typing import NamedTuple from PyQt5.QtCore import QObject, QUuid, pyqtSignal, Qt -from PyQt5.QtGui import QImage, QPainter, QColor, QBrush +from PyQt5.QtGui import QPainter, QColor, QBrush import uuid from . import eventloop, workflow, util @@ -29,7 +29,7 @@ from .connection import Connection from .properties import Property, ObservableProperties from .jobs import Job, JobKind, JobParams, JobQueue, JobState, JobRegion -from .control import ControlLayer, ControlLayerList +from .control import ControlLayer from .region import Region, RegionLink, RootRegion, process_regions, get_region_inpaint_mask from .resources import ControlMode from .resolution import compute_bounds, compute_relative_bounds From 98e64d808309e6a74d2c618931f6580ea2a87d45 Mon Sep 17 00:00:00 2001 From: modelflat Date: Tue, 26 Nov 2024 13:42:36 +0100 Subject: [PATCH 4/4] Don't exit live generation loop when switching documents --- ai_diffusion/model.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 294d40b2d..4e6c2b696 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -885,16 +885,17 @@ def handle_job_finished(self, job: Job): eventloop.run(_report_errors(self._model, self._continue_generating())) async def _continue_generating(self): - while self.is_active and self._model.document.is_active: - new_input, job_params = self._model._prepare_live_workflow() - if self._last_input != new_input: - now = time.monotonic() - if self._last_change + settings.live_redraw_grace_period <= now: - await self._model._generate_live(new_input, job_params) - self._last_input = new_input - return - else: - self._last_change = time.monotonic() + while self.is_active: + if self._model.document.is_active: + new_input, job_params = self._model._prepare_live_workflow() + if self._last_input != new_input: + now = time.monotonic() + if self._last_change + settings.live_redraw_grace_period <= now: + await self._model._generate_live(new_input, job_params) + self._last_input = new_input + return + else: + self._last_change = time.monotonic() await asyncio.sleep(self._poll_rate) def apply_result(self, layer_only=False):