m3g4p0p commited on
Commit
9e76dab
·
2 Parent(s): 3dbe9e5 a0dada5

Merge branch 'feature/random-colors'

Browse files
TODO ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Todo:
2
+ Gradio:
3
+ ☐ Upload button
4
+ ☐ Color group
5
+ CLI:
6
+ ☐ Buffer context
myapp/__main__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from myapp.cli import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
myapp/app.py CHANGED
@@ -2,7 +2,7 @@ import re
2
  from functools import partial
3
  from io import BytesIO
4
  from pathlib import Path
5
- from typing import Any
6
 
7
  import gradio as gr
8
  import segno
@@ -10,6 +10,10 @@ from gradio.components import Component
10
  from huggingface_hub import InferenceClient
11
  from PIL import Image
12
  from qrcode_artistic import write_artistic
 
 
 
 
13
 
14
  try:
15
  import dotenv
@@ -19,6 +23,7 @@ except ImportError:
19
  pass
20
 
21
  client = InferenceClient(model="black-forest-labs/FLUX.1-schnell")
 
22
 
23
  MODELS = [
24
  "stabilityai/stable-diffusion-3.5-large",
@@ -39,13 +44,20 @@ with gr.Blocks() as demo:
39
 
40
  with gr.Column():
41
  output = gr.Image()
42
- background = gr.Image("static/example.webp", visible=False, type="filepath")
 
 
43
  scale = gr.Slider(3, 15, 9, step=1, label="Scale")
 
44
 
45
  with gr.Row():
46
  color_dark = gr.ColorPicker("#000000", label="Dark")
47
  color_light = gr.ColorPicker("#FFFFFF", label="Light")
48
 
 
 
 
 
49
  def generate_background(data: dict[Component, Any]):
50
  if not data.get(prompt):
51
  return gr.skip(), gr.skip()
@@ -57,6 +69,9 @@ with gr.Blocks() as demo:
57
  return None
58
 
59
  def to_hex_format(value: str):
 
 
 
60
  if value.startswith("#"):
61
  return value
62
 
@@ -66,7 +81,7 @@ with gr.Blocks() as demo:
66
  return f"#{r:02X}{g:02X}{b:02X}"
67
 
68
  image = Image.open(data[background])
69
- qr_code = segno.make(data[text], error="h")
70
 
71
  with BytesIO() as buffer:
72
  write_artistic(
@@ -77,12 +92,23 @@ with gr.Blocks() as demo:
77
  scale=data[scale],
78
  light=to_hex_format(data[color_light]),
79
  dark=to_hex_format(data[color_dark]),
 
80
  )
81
 
82
  return Image.open(buffer)
83
 
 
 
 
 
 
 
 
 
 
 
84
  gr.on(
85
- [button.click, prompt.submit],
86
  partial(gr.update, interactive=False),
87
  outputs=button,
88
  ).then(
@@ -100,6 +126,7 @@ with gr.Blocks() as demo:
100
  text.submit,
101
  background.change,
102
  scale.change,
 
103
  color_light.change,
104
  color_dark.change,
105
  ],
@@ -108,12 +135,20 @@ with gr.Blocks() as demo:
108
  text,
109
  background,
110
  scale,
 
111
  color_light,
112
  color_dark,
113
  },
114
  outputs=output,
115
  )
116
 
 
 
 
 
 
 
 
117
 
118
  if __name__ == "__main__":
119
  demo.launch()
 
2
  from functools import partial
3
  from io import BytesIO
4
  from pathlib import Path
5
+ from typing import Any, cast
6
 
7
  import gradio as gr
8
  import segno
 
10
  from huggingface_hub import InferenceClient
11
  from PIL import Image
12
  from qrcode_artistic import write_artistic
13
+ from segno.consts import ERROR_MAPPING
14
+
15
+ from myapp.colorutils import array_to_hex
16
+ from myapp.palette import extract_color_clusters, sort_color_clusters
17
 
18
  try:
19
  import dotenv
 
23
  pass
24
 
25
  client = InferenceClient(model="black-forest-labs/FLUX.1-schnell")
26
+ static_path = Path(__file__).parent.relative_to(Path.cwd()) / "static"
27
 
28
  MODELS = [
29
  "stabilityai/stable-diffusion-3.5-large",
 
44
 
45
  with gr.Column():
46
  output = gr.Image()
47
+ background = gr.Image(
48
+ str(static_path / "example.webp"), visible=False, type="filepath"
49
+ )
50
  scale = gr.Slider(3, 15, 9, step=1, label="Scale")
51
+ error = gr.Radio(list(ERROR_MAPPING), value="H", label="Error")
52
 
53
  with gr.Row():
54
  color_dark = gr.ColorPicker("#000000", label="Dark")
55
  color_light = gr.ColorPicker("#FFFFFF", label="Light")
56
 
57
+ with gr.Row():
58
+ extract_colors = gr.Button("Extract")
59
+ gr.ClearButton([color_dark, color_light], value="Reset")
60
+
61
  def generate_background(data: dict[Component, Any]):
62
  if not data.get(prompt):
63
  return gr.skip(), gr.skip()
 
69
  return None
70
 
71
  def to_hex_format(value: str):
72
+ if value is None:
73
+ return None
74
+
75
  if value.startswith("#"):
76
  return value
77
 
 
81
  return f"#{r:02X}{g:02X}{b:02X}"
82
 
83
  image = Image.open(data[background])
84
+ qr_code = segno.make(data[text], error=data[error])
85
 
86
  with BytesIO() as buffer:
87
  write_artistic(
 
92
  scale=data[scale],
93
  light=to_hex_format(data[color_light]),
94
  dark=to_hex_format(data[color_dark]),
95
+ quiet_zone=cast(Any, "#FFFFFF"),
96
  )
97
 
98
  return Image.open(buffer)
99
 
100
+ def generate_palette(data: dict[Component, Any]):
101
+ if data[background] is None:
102
+ return None, None
103
+
104
+ image = Image.open(data[background])
105
+ k_means = extract_color_clusters(image, n_clusters=2)
106
+ primary, secondary = map(array_to_hex, sort_color_clusters(k_means))
107
+
108
+ return primary, secondary
109
+
110
  gr.on(
111
+ [button.click, prompt.submit, demo.load],
112
  partial(gr.update, interactive=False),
113
  outputs=button,
114
  ).then(
 
126
  text.submit,
127
  background.change,
128
  scale.change,
129
+ error.change,
130
  color_light.change,
131
  color_dark.change,
132
  ],
 
135
  text,
136
  background,
137
  scale,
138
+ error,
139
  color_light,
140
  color_dark,
141
  },
142
  outputs=output,
143
  )
144
 
145
+ gr.on(
146
+ [extract_colors.click],
147
+ generate_palette,
148
+ inputs={background},
149
+ outputs=[color_dark, color_light],
150
+ )
151
+
152
 
153
  if __name__ == "__main__":
154
  demo.launch()
myapp/cli.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import click
2
+ import dotenv
3
+ import segno
4
+ from huggingface_hub import InferenceClient
5
+ from qrcode_artistic import write_artistic
6
+ from segno.consts import ERROR_MAPPING
7
+
8
+ from myapp.palette import extract_color_clusters, generate_palette_image
9
+ from myapp.params import ImageParamType
10
+
11
+ dotenv.load_dotenv()
12
+ client = InferenceClient()
13
+
14
+
15
+ @click.group()
16
+ def cli():
17
+ pass
18
+
19
+
20
+ @cli.command()
21
+ @click.option("--prompt", required=True)
22
+ @click.option("--target", type=click.Path(dir_okay=False), required=True)
23
+ @click.option("--model", default="black-forest-labs/FLUX.1-schnell")
24
+ @click.option("--width", default=400)
25
+ @click.option("--height", default=400)
26
+ def generate_image(prompt, target, model, width, height):
27
+ image = client.text_to_image(
28
+ prompt=prompt,
29
+ model=model,
30
+ width=width,
31
+ height=height,
32
+ )
33
+
34
+ image.save(target)
35
+
36
+
37
+ @cli.command()
38
+ @click.option("--image", type=ImageParamType(), required=True)
39
+ @click.option("--target", type=click.Path(dir_okay=False), required=True)
40
+ @click.option("--n-colors", default=4)
41
+ @click.option("--shade", "shades", default=(0.0,), multiple=True)
42
+ def generate_palette(image, target, n_colors, shades):
43
+ k_means = extract_color_clusters(image, n_colors)
44
+ palette = generate_palette_image(k_means, shades=shades)
45
+ palette.save(target)
46
+
47
+
48
+ @cli.command()
49
+ @click.option("--text", required=True)
50
+ @click.option("--background", type=ImageParamType(), required=True)
51
+ @click.option("--target", type=click.Path(dir_okay=False), required=True)
52
+ @click.option("--scale", type=click.IntRange(min=3, max=15), default=9)
53
+ @click.option("--error", type=click.Choice(list(ERROR_MAPPING)))
54
+ def generate_qr_code(text, background, target, scale, error):
55
+ write_artistic(
56
+ segno.make(text, error=error),
57
+ background.filename,
58
+ target,
59
+ scale=scale,
60
+ )
myapp/cli/main.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import click
2
+ import dotenv
3
+ from huggingface_hub import InferenceClient
4
+
5
+ from myapp.palette import extract_color_clusters, generate_palette_image
6
+ from myapp.params import ImageParamType
7
+
8
+ dotenv.load_dotenv()
9
+ client = InferenceClient()
10
+
11
+
12
+ @click.group()
13
+ def cli():
14
+ pass
15
+
16
+
17
+ @cli.command()
18
+ @click.option("--prompt", required=True)
19
+ @click.option("--target", type=click.Path(dir_okay=False), required=True)
20
+ @click.option("--model", default="black-forest-labs/FLUX.1-schnell")
21
+ @click.option("--width", default=400)
22
+ @click.option("--height", default=400)
23
+ def generate_image(prompt, target, model, width, height):
24
+ image = client.text_to_image(
25
+ prompt=prompt,
26
+ model=model,
27
+ width=width,
28
+ height=height,
29
+ )
30
+
31
+ image.save(target)
32
+
33
+
34
+ @cli.command()
35
+ @click.option("--image", type=ImageParamType(), required=True)
36
+ @click.option("--target", type=click.Path(dir_okay=False), required=True)
37
+ @click.option("--n-colors", default=4)
38
+ @click.option("--shade", "shades", default=(0.0,), multiple=True)
39
+ def generate_palette(image, target, n_colors, shades):
40
+ k_means = extract_color_clusters(image, n_colors)
41
+ palette = generate_palette_image(k_means, shades=shades)
42
+ palette.save(target)
myapp/colorutils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import colorsys
2
+ from typing import NamedTuple
3
+
4
+ import numpy as np
5
+
6
+
7
+ class HSVUpdate(NamedTuple):
8
+ h: int = 0
9
+ s: int = 0
10
+ v: int = 0
11
+
12
+ def apply(self, color: np.ndarray):
13
+ hsv = colorsys.rgb_to_hsv(*color / 255)
14
+ tmp = np.add(hsv, self).clip(0, 1)
15
+ rgb = colorsys.hsv_to_rgb(*tmp)
16
+
17
+ return np.array(rgb) * 255
18
+
19
+
20
+ def get_hsv_value(cluster: np.ndarray):
21
+ return colorsys.rgb_to_hsv(*cluster / 255)[2]
22
+
23
+
24
+ def add_hsv_saturation(cluster: np.ndarray, delta: float):
25
+ h, s, v = colorsys.rgb_to_hsv(*cluster / 255)
26
+ s = max(0, min(1, s + delta))
27
+
28
+ return np.array(colorsys.hsv_to_rgb(h, s, v)) * 255
29
+
30
+
31
+ def array_to_hex(values: np.ndarray):
32
+ values = np.round(values).astype(int)
33
+ return "#" + ("{:02X}" * len(values)).format(*values)
myapp/palette.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import itertools
2
+
3
+ import numpy as np
4
+ from PIL import Image
5
+ from sklearn.cluster import KMeans
6
+
7
+ from myapp.colorutils import add_hsv_saturation, get_hsv_value
8
+
9
+
10
+ def extract_color_clusters(image_array: np.ndarray | Image.Image, n_clusters=2):
11
+ if not isinstance(image_array, np.ndarray):
12
+ image_array = np.array(image_array)
13
+
14
+ w, h, d = image_array.shape
15
+ pixels = image_array.reshape(w * h, d)
16
+
17
+ return KMeans(n_clusters=n_clusters).fit(pixels)
18
+
19
+
20
+ def sort_color_clusters(k_means: KMeans):
21
+ return sorted(k_means.cluster_centers_, key=get_hsv_value)
22
+
23
+
24
+ def iter_color_shades(k_means: KMeans, shades: tuple[float, ...]):
25
+ cluster_centers = sort_color_clusters(k_means)
26
+
27
+ for delta, cluster_center in itertools.product(shades, cluster_centers):
28
+ yield add_hsv_saturation(cluster_center, delta)
29
+
30
+
31
+ def generate_palette_image(k_means: KMeans, size=40, shades=(0.0,)):
32
+ num_cluster_centers = len(k_means.cluster_centers_)
33
+ image = Image.new("RGB", (num_cluster_centers * size, len(shades) * size))
34
+
35
+ for i, color in enumerate(iter_color_shades(k_means, shades)):
36
+ color = tuple(map(int, color))
37
+ part = Image.new("RGB", (size, size), color)
38
+ position = (i % num_cluster_centers * size, i // num_cluster_centers * size)
39
+ image.paste(part, position)
40
+
41
+ return image
myapp/palette_demo.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import ExitStack
2
+
3
+ import gradio as gr
4
+ import numpy as np
5
+
6
+ from myapp.colorutils import array_to_hex
7
+ from myapp.palette import extract_color_clusters, iter_color_shades
8
+
9
+ with gr.Blocks() as demo:
10
+ image = gr.Image("static/vulture.webp")
11
+ n_colors = gr.Slider(1, 16, 4, step=1)
12
+ button = gr.Button()
13
+
14
+ @gr.render(inputs=[image, n_colors])
15
+ def render_palette(image_array: np.ndarray, n_clusers: int):
16
+ model = extract_color_clusters(image_array, n_clusers)
17
+ cluster_shades = iter_color_shades(model, (0, 0.2, 0.4, 0.6))
18
+
19
+ with ExitStack() as stack:
20
+ for i, cluster in enumerate(cluster_shades):
21
+ if i % n_clusers == 0:
22
+ stack.pop_all().close()
23
+ stack.enter_context(gr.Group())
24
+ stack.enter_context(gr.Row(variant="compact"))
25
+
26
+ gr.ColorPicker(array_to_hex(cluster), container=False)
27
+
28
+
29
+ if __name__ == "__main__":
30
+ demo.launch()
myapp/params.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ import click
4
+ from PIL import Image
5
+
6
+
7
+ class ImageParamType(click.ParamType):
8
+ name = "image"
9
+
10
+ def convert(
11
+ self, value: Any, param: click.Parameter | None, ctx: click.Context | None
12
+ ) -> Any:
13
+ if value is None or isinstance(value, Image.Image):
14
+ return value
15
+
16
+ try:
17
+ return Image.open(value)
18
+ except OSError as e:
19
+ self.fail(str(e))
myapp/static/example.webp ADDED
myapp/static/vulture.webp ADDED
pyproject.toml CHANGED
@@ -8,6 +8,7 @@ dependencies = [
8
  "gradio>=5.14.0",
9
  "huggingface-hub>=0.27.1",
10
  "qrcode-artistic>=3.0.2",
 
11
  "segno>=1.6.1",
12
  ]
13
 
@@ -19,3 +20,6 @@ develop = [
19
 
20
  [tool.setuptools.packages.find]
21
  include = ["myapp"]
 
 
 
 
8
  "gradio>=5.14.0",
9
  "huggingface-hub>=0.27.1",
10
  "qrcode-artistic>=3.0.2",
11
+ "scikit-learn>=1.6.1",
12
  "segno>=1.6.1",
13
  ]
14
 
 
20
 
21
  [tool.setuptools.packages.find]
22
  include = ["myapp"]
23
+
24
+ [project.scripts]
25
+ myapp = "myapp.cli:cli"
requirements.txt CHANGED
@@ -57,6 +57,8 @@ idna==3.10
57
  # requests
58
  jinja2==3.1.5
59
  # via gradio
 
 
60
  markdown-it-py==3.0.0
61
  # via rich
62
  markupsafe==2.1.5
@@ -69,6 +71,8 @@ numpy==2.2.2
69
  # via
70
  # gradio
71
  # pandas
 
 
72
  orjson==3.10.15
73
  # via gradio
74
  packaging==24.2
@@ -112,6 +116,10 @@ ruff==0.9.4
112
  # via gradio
113
  safehttpx==0.1.6
114
  # via gradio
 
 
 
 
115
  segno==1.6.1
116
  # via
117
  # myapp (pyproject.toml)
@@ -128,6 +136,8 @@ starlette==0.41.3
128
  # via
129
  # fastapi
130
  # gradio
 
 
131
  tomlkit==0.13.2
132
  # via gradio
133
  tqdm==4.67.1
 
57
  # requests
58
  jinja2==3.1.5
59
  # via gradio
60
+ joblib==1.4.2
61
+ # via scikit-learn
62
  markdown-it-py==3.0.0
63
  # via rich
64
  markupsafe==2.1.5
 
71
  # via
72
  # gradio
73
  # pandas
74
+ # scikit-learn
75
+ # scipy
76
  orjson==3.10.15
77
  # via gradio
78
  packaging==24.2
 
116
  # via gradio
117
  safehttpx==0.1.6
118
  # via gradio
119
+ scikit-learn==1.6.1
120
+ # via myapp (pyproject.toml)
121
+ scipy==1.15.1
122
+ # via scikit-learn
123
  segno==1.6.1
124
  # via
125
  # myapp (pyproject.toml)
 
136
  # via
137
  # fastapi
138
  # gradio
139
+ threadpoolctl==3.5.0
140
+ # via scikit-learn
141
  tomlkit==0.13.2
142
  # via gradio
143
  tqdm==4.67.1
uv.lock CHANGED
@@ -386,6 +386,15 @@ wheels = [
386
  { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
387
  ]
388
 
 
 
 
 
 
 
 
 
 
389
  [[package]]
390
  name = "markdown-it-py"
391
  version = "3.0.0"
@@ -445,6 +454,7 @@ dependencies = [
445
  { name = "gradio" },
446
  { name = "huggingface-hub" },
447
  { name = "qrcode-artistic" },
 
448
  { name = "segno" },
449
  ]
450
 
@@ -461,6 +471,7 @@ requires-dist = [
461
  { name = "ipdb", marker = "extra == 'develop'", specifier = ">=0.13.13" },
462
  { name = "python-dotenv", marker = "extra == 'develop'", specifier = ">=1.0.1" },
463
  { name = "qrcode-artistic", specifier = ">=3.0.2" },
 
464
  { name = "segno", specifier = ">=1.6.1" },
465
  ]
466
 
@@ -882,6 +893,68 @@ wheels = [
882
  { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692 },
883
  ]
884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
  [[package]]
886
  name = "segno"
887
  version = "1.6.1"
@@ -953,6 +1026,15 @@ wheels = [
953
  { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
954
  ]
955
 
 
 
 
 
 
 
 
 
 
956
  [[package]]
957
  name = "tomlkit"
958
  version = "0.13.2"
 
386
  { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
387
  ]
388
 
389
+ [[package]]
390
+ name = "joblib"
391
+ version = "1.4.2"
392
+ source = { registry = "https://pypi.org/simple" }
393
+ sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 }
394
+ wheels = [
395
+ { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 },
396
+ ]
397
+
398
  [[package]]
399
  name = "markdown-it-py"
400
  version = "3.0.0"
 
454
  { name = "gradio" },
455
  { name = "huggingface-hub" },
456
  { name = "qrcode-artistic" },
457
+ { name = "scikit-learn" },
458
  { name = "segno" },
459
  ]
460
 
 
471
  { name = "ipdb", marker = "extra == 'develop'", specifier = ">=0.13.13" },
472
  { name = "python-dotenv", marker = "extra == 'develop'", specifier = ">=1.0.1" },
473
  { name = "qrcode-artistic", specifier = ">=3.0.2" },
474
+ { name = "scikit-learn", specifier = ">=1.6.1" },
475
  { name = "segno", specifier = ">=1.6.1" },
476
  ]
477
 
 
893
  { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692 },
894
  ]
895
 
896
+ [[package]]
897
+ name = "scikit-learn"
898
+ version = "1.6.1"
899
+ source = { registry = "https://pypi.org/simple" }
900
+ dependencies = [
901
+ { name = "joblib" },
902
+ { name = "numpy" },
903
+ { name = "scipy" },
904
+ { name = "threadpoolctl" },
905
+ ]
906
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 }
907
+ wheels = [
908
+ { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 },
909
+ { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 },
910
+ { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 },
911
+ { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 },
912
+ { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 },
913
+ { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 },
914
+ { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 },
915
+ { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 },
916
+ { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 },
917
+ { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 },
918
+ { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 },
919
+ { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 },
920
+ { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 },
921
+ { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 },
922
+ ]
923
+
924
+ [[package]]
925
+ name = "scipy"
926
+ version = "1.15.1"
927
+ source = { registry = "https://pypi.org/simple" }
928
+ dependencies = [
929
+ { name = "numpy" },
930
+ ]
931
+ sdist = { url = "https://files.pythonhosted.org/packages/76/c6/8eb0654ba0c7d0bb1bf67bf8fbace101a8e4f250f7722371105e8b6f68fc/scipy-1.15.1.tar.gz", hash = "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6", size = 59407493 }
932
+ wheels = [
933
+ { url = "https://files.pythonhosted.org/packages/d8/6e/a9c42d0d39e09ed7fd203d0ac17adfea759cba61ab457671fe66e523dbec/scipy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776", size = 41478318 },
934
+ { url = "https://files.pythonhosted.org/packages/04/ee/e3e535c81828618878a7433992fecc92fa4df79393f31a8fea1d05615091/scipy-1.15.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f", size = 32596696 },
935
+ { url = "https://files.pythonhosted.org/packages/c4/5e/b1b0124be8e76f87115f16b8915003eec4b7060298117715baf13f51942c/scipy-1.15.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04", size = 24870366 },
936
+ { url = "https://files.pythonhosted.org/packages/14/36/c00cb73eefda85946172c27913ab995c6ad4eee00fa4f007572e8c50cd51/scipy-1.15.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9", size = 28007461 },
937
+ { url = "https://files.pythonhosted.org/packages/68/94/aff5c51b3799349a9d1e67a056772a0f8a47db371e83b498d43467806557/scipy-1.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce", size = 38068174 },
938
+ { url = "https://files.pythonhosted.org/packages/b0/3c/0de11ca154e24a57b579fb648151d901326d3102115bc4f9a7a86526ce54/scipy-1.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2", size = 40249869 },
939
+ { url = "https://files.pythonhosted.org/packages/15/09/472e8d0a6b33199d1bb95e49bedcabc0976c3724edd9b0ef7602ccacf41e/scipy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5", size = 42629068 },
940
+ { url = "https://files.pythonhosted.org/packages/ff/ba/31c7a8131152822b3a2cdeba76398ffb404d81d640de98287d236da90c49/scipy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d", size = 43621992 },
941
+ { url = "https://files.pythonhosted.org/packages/2b/bf/dd68965a4c5138a630eeed0baec9ae96e5d598887835bdde96cdd2fe4780/scipy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85", size = 41441136 },
942
+ { url = "https://files.pythonhosted.org/packages/ef/5e/4928581312922d7e4d416d74c416a660addec4dd5ea185401df2269ba5a0/scipy-1.15.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692", size = 32533699 },
943
+ { url = "https://files.pythonhosted.org/packages/32/90/03f99c43041852837686898c66767787cd41c5843d7a1509c39ffef683e9/scipy-1.15.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab", size = 24807289 },
944
+ { url = "https://files.pythonhosted.org/packages/9d/52/bfe82b42ae112eaba1af2f3e556275b8727d55ac6e4932e7aef337a9d9d4/scipy-1.15.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c", size = 27929844 },
945
+ { url = "https://files.pythonhosted.org/packages/f6/77/54ff610bad600462c313326acdb035783accc6a3d5f566d22757ad297564/scipy-1.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e", size = 38031272 },
946
+ { url = "https://files.pythonhosted.org/packages/f1/26/98585cbf04c7cf503d7eb0a1966df8a268154b5d923c5fe0c1ed13154c49/scipy-1.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e", size = 40210217 },
947
+ { url = "https://files.pythonhosted.org/packages/fd/3f/3d2285eb6fece8bc5dbb2f9f94d61157d61d155e854fd5fea825b8218f12/scipy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4", size = 42587785 },
948
+ { url = "https://files.pythonhosted.org/packages/48/7d/5b5251984bf0160d6533695a74a5fddb1fa36edd6f26ffa8c871fbd4782a/scipy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef", size = 43640439 },
949
+ { url = "https://files.pythonhosted.org/packages/e7/b8/0e092f592d280496de52e152582030f8a270b194f87f890e1a97c5599b81/scipy-1.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b", size = 41619862 },
950
+ { url = "https://files.pythonhosted.org/packages/f6/19/0b6e1173aba4db9e0b7aa27fe45019857fb90d6904038b83927cbe0a6c1d/scipy-1.15.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95", size = 32610387 },
951
+ { url = "https://files.pythonhosted.org/packages/e7/02/754aae3bd1fa0f2479ade3cfdf1732ecd6b05853f63eee6066a32684563a/scipy-1.15.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364", size = 24883814 },
952
+ { url = "https://files.pythonhosted.org/packages/1f/ac/d7906201604a2ea3b143bb0de51b3966f66441ba50b7dc182c4505b3edf9/scipy-1.15.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0", size = 27944865 },
953
+ { url = "https://files.pythonhosted.org/packages/84/9d/8f539002b5e203723af6a6f513a45e0a7671e9dabeedb08f417ac17e4edc/scipy-1.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54", size = 39883261 },
954
+ { url = "https://files.pythonhosted.org/packages/97/c0/62fd3bab828bcccc9b864c5997645a3b86372a35941cdaf677565c25c98d/scipy-1.15.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c", size = 42093299 },
955
+ { url = "https://files.pythonhosted.org/packages/e4/1f/5d46a8d94e9f6d2c913cbb109e57e7eed914de38ea99e2c4d69a9fc93140/scipy-1.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5", size = 43181730 },
956
+ ]
957
+
958
  [[package]]
959
  name = "segno"
960
  version = "1.6.1"
 
1026
  { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
1027
  ]
1028
 
1029
+ [[package]]
1030
+ name = "threadpoolctl"
1031
+ version = "3.5.0"
1032
+ source = { registry = "https://pypi.org/simple" }
1033
+ sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 }
1034
+ wheels = [
1035
+ { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 },
1036
+ ]
1037
+
1038
  [[package]]
1039
  name = "tomlkit"
1040
  version = "0.13.2"