├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── scripts
└── detail_daemon.py
└── style.css
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sahand Ahmadian
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Detail Daemon
2 | This is an extension for [Stable Diffusion Web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui), which allows users to adjust the amount of detail/smoothness in an image, during the sampling steps.
3 |
4 | It uses no LORAs, ControlNets, etc., and as a result its performance is not biased towards any certain style and it introduces no new stylistic or semantic features of its own into the generation. This also means that it can work with any model and on any type of image.
5 |
6 | *Model: SSD-1B*
7 | 
8 | *Left: Less detail, Middle: Original, Right: More detail*
9 |
10 | *Model: SD 1.5 (finetuned)*
11 | 
12 | *Left: Less detail, Middle: Original, Right: More detail*
13 |
14 |
15 | ## How It Works
16 | Detail Daemon works by manipulating the original noise levels at every sampling step, according to a customizable schedule.
17 |
18 | ### In Theory
19 | The noise levels (sigmas, i.e. the standard deviation of the noise) tell the model how much noise it should expect, and try to remove, at each denoising step. A higher sigma value at a certain denoising step tells the model to denoise more aggressively at that step and vice versa.
20 |
21 | With a common sigmas schedule, the sigmas start at very high values at the beginning of the denoising process, then quickly fall to low values in the middle, and to very low values towards the end of the process. This curve (along with the timesteps schedule, but that's a story for another day) is what makes it so that larger features (low frequencies) of the image are defined at the earlier steps, and towards the end of the process you can only see minor changes in the smaller features (high frequencies). We'll get back to this later.
22 |
23 | Now, if we pass the model a sigmas schedule with values lower than the original, at each step the model will denoise less, resulting a noisier output latent at that step. But then in the steps after that, the model does its best to make sense of this extra noise and turn it into image features. So in theory, *when done in modesty*, this would result in a more detailed image. If you push it too hard, the model won't be able to handle the extra noise added at each step and the end result will devolve into pure noise. So modesty is key.
24 |
25 | ### But in Practice
26 | Modesty only gets you so far! Also, wtf are those? As the examples below show, you can't really add that much detail to the image before it either breaks down, and/or becomes a totally different thing.
27 |
28 | *SD 1.5*
29 | 
30 | Original sigmas (left) multiplied by .9, .85, .8
31 |
32 | *SDXL*
33 | 
34 | Original sigmas (left) multiplied by .95, .9, .85, .875, .8
35 |
36 | That's because:
37 | 1. We're constantly adding noise and not giving the model enough time to deal with it
38 | 2. We are manipulating the early steps where the low frequency features of the image (color, composition, etc.) are defined
39 |
40 | ### Enter the Schedule
41 | What we usually mean by "detail" falls within the mid to high frequency range, which correspond to the middle to late steps in the sampling process. So if we skip the early steps to leave the main features of the image intact, and the late steps to give the model some time to turn the extra noise into useful detail, we'll have something like this:
42 |
43 | 
44 |
45 | Then we could make our schedule a bit fancier and have it target specific steps corresponding to different sized details:
46 |
47 | 
48 |
49 | Which steps correspond to which exact frequency range depends on the model you're using, the sampler, your prompt (specially if you're using Prompt Editing and stuff), and probably a bunch of other things. There are also fancier things you can (and should) do with the schedule, like pushing the sigmas too low for some heavy extra noise and then too high to clean up the excess and leave some nice details. So you need to do some tweaking to figure out the best schedule for each image you generate, or at least the ones that need their level of detail adjusted. But ideally you should be spending countless hours of your life sculpting the perfect detail adjustment schedule for every image, cuz that's why we're here.
50 |
51 | I'll soon provide specific examples addressing different scenarios and some of the techniques I've come up with. (note to self: move these to the wiki page)
52 |
53 | ## Installation
54 | Open SD WebUI > Go to Extensions tab > Go to Available tab > Click Load from: > Find Detail Daemon > Click Install
55 |
56 | Or Go to Install from URL tab > Paste this repo's URL into the first field > Click Install
57 |
58 | Or go to your WebUI folder and manually clone this repo into your extensions folder:
59 |
60 | `git clone "https://github.com/muerrilla/sd-webui-detail-daemon" extensions/sd-webui-detail-daemon`
61 |
62 | ## Getting Started
63 | After installation you can find the extension in your txt2img and img2img tabs.
64 | 
65 | ### Sliders:
66 | The sliders (and that one checkbox) set the amount of adjustment (positive values → add detail, negative values → remove detail) and the sampling steps during which it is applied (i.e. the schedule). So the X axis of the graph is your sampling steps, normalized to the (0,1) range, and the Y axis is the amount of adjustment. The rest is pretty self-explanatory I think. Just drag things and look at the graph for changes.
67 | ### Numbers:
68 | The three number inputs at the buttom are provided because sometimes the slider max/mins are too limiting.
69 | ### Modes:
70 | The `cond` and `uncond` modes affect only their respective latents, while `both` affects both (duh!). The `cond` and `uncond` modes are less intense and also allow changes to be applied at earlier steps without diverging too far from the original generation, since the other latent stays intact.
71 |
72 | There's also a minor twist: in the `both` mode if `detail amount` is positive both cond and uncond latents become more detailed. So the more detailed cond latent will try to push the generation towards more detail, while the more detailed uncond latent will try to push towards less detail. This causes more new features/artifacts to pop into the image in this mode.
73 |
74 | ### Tips:
75 | I'll write up some proper docs on how best to set the parameters, as soon as possible. For now you gotta play around with the sliders and figure out how the shape of the schedule affects the image. I suggest you set your live preview update period to every frame, or every other frame, so you could see clearly what's going on at every step of the sampling process and how Detail Daemon affects it, till you get a good grasp of how this thing works.
76 |
77 | ## Notes:
78 | - Doesn't support Compositional Diffusion (i.e. the AND syntax) properly. Specially if you have a batch size > 1 or negative weights in your prompts, and the mode is set to `cond` or `uncond`.
79 | - It's probably impossible to use or very hard to control with few-step models (Turbo, Lightning, etc.). Edit: It's managable.
80 | - It works with Forge (`cond` and `uncond` modes are not supported).
81 | - It's not the same as AlignYourSteps, FreeU, etc.
82 | - It is similar (in what it sets out to do, not in how it does it) to the [ReSharpen Extension](https://github.com/Haoming02/sd-webui-resharpen) by Haoming.
83 |
--------------------------------------------------------------------------------
/scripts/detail_daemon.py:
--------------------------------------------------------------------------------
1 | import os
2 | import gradio as gr
3 | import numpy as np
4 | from tqdm import tqdm
5 |
6 | import matplotlib
7 | matplotlib.use('Agg')
8 | import matplotlib.pyplot as plt
9 |
10 | import modules.scripts as scripts
11 | from modules.script_callbacks import on_cfg_denoiser, remove_callbacks_for_function, on_infotext_pasted
12 | from modules.ui_components import InputAccordion
13 |
14 |
15 | def parse_infotext(infotext, params):
16 | try:
17 | d = {}
18 | for s in params['Detail Daemon'].split(','):
19 | k, _, v = s.partition(':')
20 | d[k.strip()] = v.strip()
21 | params['Detail Daemon'] = d
22 | except Exception:
23 | pass
24 |
25 |
26 | on_infotext_pasted(parse_infotext)
27 |
28 |
29 | class Script(scripts.Script):
30 |
31 | def __init__(self):
32 | super().__init__()
33 | self.schedule_params: dict[str, float] = None
34 | self.schedule = None
35 |
36 | def title(self):
37 | return "Detail Daemon"
38 |
39 | def show(self, is_img2img):
40 | return scripts.AlwaysVisible
41 |
42 | def ui(self, is_img2img):
43 | with InputAccordion(False, label="Detail Daemon", elem_id=self.elem_id('detail-daemon')) as gr_enabled:
44 | with gr.Row():
45 | with gr.Column(scale=2):
46 | gr_amount_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.10, label="Detail Amount")
47 | gr_start = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.2, label="Start")
48 | gr_end = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.8, label="End")
49 | gr_bias = gr.Slider(minimum=0.0, maximum=1.0, step=.01, value=0.5, label="Bias")
50 | with gr.Column(scale=1, min_width=275):
51 | preview = self.visualize(False, 0.2, 0.8, 0.5, 0.1, 1, 0, 0, 0, True)
52 | gr_vis = gr.Plot(value=preview, elem_classes=['detail-daemon-vis'], show_label=False)
53 | with gr.Accordion("More Knobs:", elem_classes=['detail-daemon-more-accordion'], open=False):
54 | with gr.Row():
55 | with gr.Column(scale=2):
56 | with gr.Row():
57 | gr_start_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="Start Offset", min_width=60)
58 | gr_end_offset_slider = gr.Slider(minimum=-1.00, maximum=1.00, step=.01, value=0.00, label="End Offset", min_width=60)
59 | with gr.Row():
60 | gr_exponent = gr.Slider(minimum=0.0, maximum=10.0, step=.05, value=1.0, label="Exponent", min_width=60)
61 | gr_fade = gr.Slider(minimum=0.0, maximum=1.0, step=.05, value=0.0, label="Fade", min_width=60)
62 | # Because the slider max and min are sometimes too limiting:
63 | with gr.Row():
64 | gr_amount = gr.Number(value=0.10, precision=4, step=.01, label="Amount", min_width=60)
65 | gr_start_offset = gr.Number(value=0.0, precision=4, step=.01, label="Start Offset", min_width=60)
66 | gr_end_offset = gr.Number(value=0.0, precision=4, step=.01, label="End Offset", min_width=60)
67 | with gr.Column(scale=1, min_width=275):
68 | gr_mode = gr.Dropdown(["both", "cond", "uncond"], value="uncond", label="Mode", show_label=True, min_width=60, elem_classes=['detail-daemon-mode'])
69 | gr_smooth = gr.Checkbox(label="Smooth", value=True, min_width=60, elem_classes=['detail-daemon-smooth'])
70 | gr.Markdown("## [Ⓗ Help](https://github.com/muerrilla/sd-webui-detail-daemon)", elem_classes=['detail-daemon-help'])
71 |
72 | gr_amount_slider.release(None, gr_amount_slider, gr_amount, _js="(x) => x")
73 | gr_amount.change(None, gr_amount, gr_amount_slider, _js="(x) => x")
74 |
75 | gr_start_offset_slider.release(None, gr_start_offset_slider, gr_start_offset, _js="(x) => x")
76 | gr_start_offset.change(None, gr_start_offset, gr_start_offset_slider, _js="(x) => x")
77 |
78 | gr_end_offset_slider.release(None, gr_end_offset_slider, gr_end_offset, _js="(x) => x")
79 | gr_end_offset.change(None, gr_end_offset, gr_end_offset_slider, _js="(x) => x")
80 |
81 | vis_args = [gr_enabled, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth]
82 | for vis_arg in vis_args:
83 | if isinstance(vis_arg, gr.components.Slider):
84 | vis_arg.release(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis])
85 | else:
86 | vis_arg.change(fn=self.visualize, show_progress=False, inputs=vis_args, outputs=[gr_vis])
87 |
88 | def extract_infotext(d: dict, key, old_key):
89 | if 'Detail Daemon' in d:
90 | return d['Detail Daemon'].get(key)
91 | return d.get(old_key)
92 |
93 | self.infotext_fields = [
94 | (gr_enabled, lambda d: True if ('Detail Daemon' in d or 'DD_enabled' in d) else False),
95 | (gr_mode, lambda d: extract_infotext(d, 'mode', 'DD_mode')),
96 | (gr_amount, lambda d: extract_infotext(d, 'amount', 'DD_amount')),
97 | (gr_start, lambda d: extract_infotext(d, 'st', 'DD_start')),
98 | (gr_end, lambda d: extract_infotext(d, 'ed', 'DD_end')),
99 | (gr_bias, lambda d: extract_infotext(d, 'bias', 'DD_bias')),
100 | (gr_exponent, lambda d: extract_infotext(d, 'exp', 'DD_exponent')),
101 | (gr_start_offset, lambda d: extract_infotext(d, 'st_offset', 'DD_start_offset')),
102 | (gr_end_offset, lambda d: extract_infotext(d, 'ed_offset', 'DD_end_offset')),
103 | (gr_fade, lambda d: extract_infotext(d, 'fade', 'DD_fade')),
104 | (gr_smooth, lambda d: extract_infotext(d, 'smooth', 'DD_smooth')),
105 | ]
106 | return [gr_enabled, gr_mode, gr_start, gr_end, gr_bias, gr_amount, gr_exponent, gr_start_offset, gr_end_offset, gr_fade, gr_smooth]
107 |
108 | def process(self, p, enabled, mode, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth):
109 |
110 | enabled = getattr(p, "DD_enabled", enabled)
111 | mode = getattr(p, "DD_mode", mode)
112 | amount = getattr(p, "DD_amount", amount)
113 | start = getattr(p, "DD_start", start)
114 | end = getattr(p, "DD_end", end)
115 | bias = getattr(p, "DD_bias", bias)
116 | exponent = getattr(p, "DD_exponent", exponent)
117 | start_offset = getattr(p, "DD_start_offset", start_offset)
118 | end_offset = getattr(p, "DD_end_offset", end_offset)
119 | fade = getattr(p, "DD_fade", fade)
120 | smooth = getattr(p, "DD_smooth", smooth)
121 |
122 | if enabled:
123 | if p.sampler_name in ["DPM adaptive", "HeunPP2"]:
124 | tqdm.write(f'\033[33mWARNING:\033[0m Detail Daemon does not work with {p.sampler_name}')
125 | return
126 |
127 | self.schedule_params = {
128 | "start": start,
129 | "end": end,
130 | "bias": bias,
131 | "amount": amount,
132 | "exponent": exponent,
133 | "start_offset": start_offset,
134 | "end_offset": end_offset,
135 | "fade": fade,
136 | "smooth": smooth
137 | }
138 | self.mode = mode
139 | self.cfg_scale = p.cfg_scale
140 | self.batch_size = p.batch_size
141 | on_cfg_denoiser(self.denoiser_callback)
142 | self.callback_added = True
143 | p.extra_generation_params['Detail Daemon'] = f'mode:{mode},amount:{amount},st:{start},ed:{end},bias:{bias},exp:{exponent},st_offset:{start_offset},ed_offset:{end_offset},fade:{fade},smooth:{1 if smooth else 0}'
144 | tqdm.write('\033[32mINFO:\033[0m Detail Daemon is enabled')
145 | else:
146 | if hasattr(self, 'callback_added'):
147 | remove_callbacks_for_function(self.denoiser_callback)
148 | delattr(self, 'callback_added')
149 | self.schedule = None
150 | # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m')
151 |
152 | def before_process_batch(self, p, *args, **kwargs):
153 | self.is_hires = False
154 |
155 | def postprocess(self, p, processed, *args):
156 | if hasattr(self, 'callback_added'):
157 | remove_callbacks_for_function(self.denoiser_callback)
158 | delattr(self, 'callback_added')
159 | self.schedule = None
160 | # tqdm.write('\033[90mINFO: Detail Daemon callback removed\033[0m')
161 |
162 | def before_hr(self, p, *args):
163 | self.is_hires = True
164 | enabled = args[0]
165 | if enabled:
166 | tqdm.write(f'\033[33mINFO:\033[0m Detail Daemon does not work during Hires Fix')
167 |
168 | def denoiser_callback(self, params):
169 | if self.is_hires:
170 | return
171 | step = max(params.sampling_step, params.denoiser.step)
172 | total_steps = max(params.total_sampling_steps, params.denoiser.total_steps)
173 | corrected_step_count = total_steps - max(total_steps // params.denoiser.steps - 1, 0)
174 | if self.schedule is None:
175 | self.schedule = self.make_schedule(corrected_step_count, **self.schedule_params)
176 |
177 | idx = min(step, corrected_step_count - 1)
178 | multiplier = self.schedule[idx] * .1
179 | mode = self.mode
180 | if params.sigma.size(0) == 1 and mode != "both":
181 | mode = "both"
182 | if idx == 0:
183 | tqdm.write(f'\033[33mWARNING:\033[0m Forge does not support `cond` and `uncond` modes, using `both` instead')
184 | if mode == "cond":
185 | for i in range(self.batch_size):
186 | params.sigma[i] *= 1 - multiplier
187 | elif mode == "uncond":
188 | for i in range(self.batch_size):
189 | params.sigma[self.batch_size + i] *= 1 + multiplier
190 | else:
191 | params.sigma *= 1 - multiplier * self.cfg_scale
192 |
193 | def make_schedule(self, steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth):
194 | start = min(start, end)
195 | mid = start + bias * (end - start)
196 | multipliers = np.zeros(steps)
197 |
198 | start_idx, mid_idx, end_idx = [int(round(x * (steps - 1))) for x in [start, mid, end]]
199 |
200 | start_values = np.linspace(0, 1, mid_idx - start_idx + 1)
201 | if smooth:
202 | start_values = 0.5 * (1 - np.cos(start_values * np.pi))
203 | start_values = start_values ** exponent
204 | if start_values.any():
205 | start_values *= (amount - start_offset)
206 | start_values += start_offset
207 |
208 | end_values = np.linspace(1, 0, end_idx - mid_idx + 1)
209 | if smooth:
210 | end_values = 0.5 * (1 - np.cos(end_values * np.pi))
211 | end_values = end_values ** exponent
212 | if end_values.any():
213 | end_values *= (amount - end_offset)
214 | end_values += end_offset
215 |
216 | multipliers[start_idx:mid_idx+1] = start_values
217 | multipliers[mid_idx:end_idx+1] = end_values
218 | multipliers[:start_idx] = start_offset
219 | multipliers[end_idx+1:] = end_offset
220 | multipliers *= 1 - fade
221 |
222 | return multipliers
223 |
224 | def visualize(self, enabled, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth):
225 | try:
226 | steps = 50
227 | values = self.make_schedule(steps, start, end, bias, amount, exponent, start_offset, end_offset, fade, smooth)
228 | mean = sum(values)/steps
229 | peak = np.clip(max(abs(values)), -1, 1)
230 | if start > end:
231 | start = end
232 | mid = start + bias * (end - start)
233 | opacity = .1 + (1 - fade) * 0.7
234 | plot_color = (0.5, 0.5, 0.5, opacity) if not enabled else ((1 - peak)**2, 1, 0, opacity) if mean >= 0 else (1, (1 - peak)**2, 0, opacity)
235 | plt.rcParams.update({
236 | "text.color": plot_color,
237 | "axes.labelcolor": plot_color,
238 | "axes.edgecolor": plot_color,
239 | "figure.facecolor": (0.0, 0.0, 0.0, 0.0),
240 | "axes.facecolor": (0.0, 0.0, 0.0, 0.0),
241 | "ytick.labelsize": 6,
242 | "ytick.labelcolor": plot_color,
243 | "ytick.color": plot_color,
244 | })
245 | fig, ax = plt.subplots(figsize=(2.15, 2.00), layout="constrained")
246 | ax.plot(range(steps), values, color=plot_color)
247 | ax.axhline(y=0, color=plot_color, linestyle='dotted')
248 | ax.axvline(x=mid * (steps - 1), color=plot_color, linestyle='dotted')
249 | ax.tick_params(right=False, color=plot_color)
250 | ax.set_xticks([i * (steps - 1) / 10 for i in range(10)][1:])
251 | ax.set_xticklabels([])
252 | ax.set_ylim([-1, 1])
253 | ax.set_xlim([0, steps-1])
254 | plt.close()
255 | self.last_vis = fig
256 | return fig
257 | except Exception:
258 | if self.last_vis is not None:
259 | return self.last_vis
260 | return
261 |
262 |
263 | def xyz_support():
264 | for scriptDataTuple in scripts.scripts_data:
265 | if os.path.basename(scriptDataTuple.path) == 'xyz_grid.py':
266 | xy_grid = scriptDataTuple.module
267 |
268 | def confirm_mode(p, xs):
269 | for x in xs:
270 | if x not in ['both', 'cond', 'uncond']:
271 | raise RuntimeError(f'Invalid Detail Daemon Mode: {x}')
272 | mode = xy_grid.AxisOption(
273 | '[Detail Daemon] Mode',
274 | str,
275 | xy_grid.apply_field('DD_mode'),
276 | confirm=confirm_mode
277 | )
278 | amount = xy_grid.AxisOption(
279 | '[Detail Daemon] Amount',
280 | float,
281 | xy_grid.apply_field('DD_amount')
282 | )
283 | start = xy_grid.AxisOption(
284 | '[Detail Daemon] Start',
285 | float,
286 | xy_grid.apply_field('DD_start')
287 | )
288 | end = xy_grid.AxisOption(
289 | '[Detail Daemon] End',
290 | float,
291 | xy_grid.apply_field('DD_end')
292 | )
293 | bias = xy_grid.AxisOption(
294 | '[Detail Daemon] Bias',
295 | float,
296 | xy_grid.apply_field('DD_bias')
297 | )
298 | exponent = xy_grid.AxisOption(
299 | '[Detail Daemon] Exponent',
300 | float,
301 | xy_grid.apply_field('DD_exponent')
302 | )
303 | start_offset = xy_grid.AxisOption(
304 | '[Detail Daemon] Start Offset',
305 | float,
306 | xy_grid.apply_field('DD_start_offset')
307 | )
308 | end_offset = xy_grid.AxisOption(
309 | '[Detail Daemon] End Offset',
310 | float,
311 | xy_grid.apply_field('DD_end_offset')
312 | )
313 | fade = xy_grid.AxisOption(
314 | '[Detail Daemon] Fade',
315 | float,
316 | xy_grid.apply_field('DD_fade')
317 | )
318 | xy_grid.axis_options.extend([
319 | mode,
320 | amount,
321 | start,
322 | end,
323 | bias,
324 | exponent,
325 | start_offset,
326 | end_offset,
327 | fade,
328 | ])
329 |
330 |
331 | try:
332 | xyz_support()
333 | except Exception as e:
334 | print(f'Error trying to add XYZ plot options for Detail Daemon', e)
335 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | .detail-daemon-more-accordion {
2 | margin-top: 1em !important;
3 | }
4 |
5 | .detail-daemon-mode {
6 | margin-left: 3em !important;
7 | width: 75% !important;
8 | }
9 |
10 | .detail-daemon-smooth {
11 | margin-left: 3em !important;
12 | }
13 |
14 | .detail-daemon-vis {
15 | margin: auto !important;
16 | }
17 |
18 | .detail-daemon-help {
19 | margin: auto 1.5em !important;
20 | }
21 |
--------------------------------------------------------------------------------