carpelan commited on
Commit
d3fc046
·
1 Parent(s): 688de79

Fixed some texts and added PDF upload functionality

Browse files
app/content/sidebar.md CHANGED
@@ -1,19 +1,19 @@
1
  ## HTRflow Demo
2
-
3
- Welcome to the **HTRflow Demo** – a web application developed by the **National Archives of Sweden** in collaboration with [Huminfra](https://www.huminfra.se/). This demo lets you explore how AI transforms historical manuscripts into digital text using [HTRflow](https://ai-riksarkivet.github.io/htrflow/latest).
4
 
5
  > Note: This demo application is for demonstration purposes only and is not intended for production use.
6
  > The application is hosted on Hugging Face 🤗 using shared infrastructure, which means there is a daily quota limit on how much you can use the app each day.
7
 
8
- ### Contact
9
-
10
11
-
12
- ### Projects
13
-
14
  Both the App and HTRflow's code are completely open source. Explore and contribute on GitHub:
15
 
16
  - [APP](https://github.com/AI-Riksarkivet/htrflow_app).
17
  - [HTRflow](https://ai-riksarkivet.github.io/htrflow/latest).
18
 
19
  If you find our projects useful, please consider giving us a star ⭐!
 
 
 
 
 
 
 
 
1
  ## HTRflow Demo
2
+ Developed by the **National Archives of Sweden** with [Huminfra](https://www.huminfra.se/) that demonstrates AI-powered conversion of historical manuscripts to digital text using [HTRflow](https://ai-riksarkivet.github.io/htrflow/latest).
 
3
 
4
  > Note: This demo application is for demonstration purposes only and is not intended for production use.
5
  > The application is hosted on Hugging Face 🤗 using shared infrastructure, which means there is a daily quota limit on how much you can use the app each day.
6
 
 
 
 
 
 
 
7
  Both the App and HTRflow's code are completely open source. Explore and contribute on GitHub:
8
 
9
  - [APP](https://github.com/AI-Riksarkivet/htrflow_app).
10
  - [HTRflow](https://ai-riksarkivet.github.io/htrflow/latest).
11
 
12
  If you find our projects useful, please consider giving us a star ⭐!
13
+
14
+
15
+ ### Contact
16
+
17
18
+
19
+
app/pipelines.py CHANGED
@@ -1,7 +1,7 @@
1
  PIPELINES = {
2
  "Swedish - Spreads": {
3
  "file": "app/assets/templates/nested_swe_ra.yaml",
4
- "description": "This pipeline works well on handwritten historic documents written in Swedish with multiple text regions. The model is devloped by <a href='https://huggingface.co/Riksarkivet'>the National Archives of Sweden</a>.",
5
  "examples": [
6
  "R0003364_00005.jpg",
7
  "30002027_00008.jpg",
@@ -10,7 +10,7 @@ PIPELINES = {
10
  },
11
  "Swedish - Single page and snippets": {
12
  "file": "app/assets/templates/simple_swe_ra.yaml",
13
- "description": "This pipeline works well on handwritten historic letters and other documents written in Swedish with only one text region. The model is devloped by <a href='https://huggingface.co/Riksarkivet'>the National Archives of Sweden</a>.",
14
  "examples": [
15
  "451511_1512_01.jpg",
16
  "A0062408_00006.jpg",
@@ -23,14 +23,14 @@ PIPELINES = {
23
  "description": "This pipeline works well on handwritten historic letters and other documents written in Norwegian with only one text region. The model is developed by the <a href='https://huggingface.co/Sprakbanken/TrOCR-norhand-v3'>Language Bank</a> at The National Library of Norway.",
24
  "examples": ["norhand_fmgh040_4.jpg"],
25
  },
26
- "Medival - Single page and snippets": {
27
  "file": "app/assets/templates/simple_medival.yaml",
28
- "description": "This pipeline works well for medieval scripts written in single-page running text. It is a base model from <a href='https://huggingface.co/medieval-data'>Medieval Data</a>, but other models can be selected from here: <a href='https://huggingface.co/collections/medieval-data/trocr-medieval-htr-66871faba03abfbb1b66ab69'>Medieval Models</a>.",
29
  "examples": ["manusscript_kb.png"],
30
  },
31
  "English - Single page and snippets": {
32
  "file": "app/assets/templates/simple_eng_modern.yaml",
33
- "description": "This pipeline works well for English text in single page running text. This a base model from <a href='https://huggingface.co/microsoft/trocr-base-handwritten'>Microsoft</a>.",
34
  "examples": ["iam.png"],
35
  },
36
  }
 
1
  PIPELINES = {
2
  "Swedish - Spreads": {
3
  "file": "app/assets/templates/nested_swe_ra.yaml",
4
+ "description": "This pipeline works well on handwritten historic documents written in Swedish with multiple text regions. The HTR model used in the pipeline is <b>Swedish Lion Libre</b> from <a href='https://huggingface.co/Riksarkivet'>the National Archives of Sweden</a>.",
5
  "examples": [
6
  "R0003364_00005.jpg",
7
  "30002027_00008.jpg",
 
10
  },
11
  "Swedish - Single page and snippets": {
12
  "file": "app/assets/templates/simple_swe_ra.yaml",
13
+ "description": "This pipeline works well on handwritten historic letters and other documents written in Swedish with only one text region. The HTR model used in the pipeline is <b>Swedish Lion Libre</b> from <a href='https://huggingface.co/Riksarkivet'>the National Archives of Sweden</a>.",
14
  "examples": [
15
  "451511_1512_01.jpg",
16
  "A0062408_00006.jpg",
 
23
  "description": "This pipeline works well on handwritten historic letters and other documents written in Norwegian with only one text region. The model is developed by the <a href='https://huggingface.co/Sprakbanken/TrOCR-norhand-v3'>Language Bank</a> at The National Library of Norway.",
24
  "examples": ["norhand_fmgh040_4.jpg"],
25
  },
26
+ "Medieval - Single page and snippets": {
27
  "file": "app/assets/templates/simple_medival.yaml",
28
+ "description": "This pipeline works well for medieval scripts written in single-page running text. The HTR model is from <a href='https://huggingface.co/medieval-data'>Medieval Data</a>, but other models can be selected from here: <a href='https://huggingface.co/collections/medieval-data/trocr-medieval-htr-66871faba03abfbb1b66ab69'>Medieval Models</a>.",
29
  "examples": ["manusscript_kb.png"],
30
  },
31
  "English - Single page and snippets": {
32
  "file": "app/assets/templates/simple_eng_modern.yaml",
33
+ "description": "This pipeline works well for English text in single page running text. The HTR model is from <a href='https://huggingface.co/microsoft/trocr-base-handwritten'>Microsoft</a>.",
34
  "examples": ["iam.png"],
35
  },
36
  }
app/tabs/submit.py CHANGED
@@ -13,6 +13,7 @@ from gradio_modal import Modal
13
  from htrflow.pipeline.pipeline import Pipeline
14
  from htrflow.pipeline.steps import init_step
15
  from htrflow.volume.volume import Collection
 
16
 
17
  from app.pipelines import PIPELINES
18
 
@@ -36,7 +37,12 @@ class PipelineWithProgress(Pipeline):
36
  @classmethod
37
  def from_config(cls, config: dict[str, str]):
38
  """Init pipeline from config, ensuring the correct subclass is instantiated."""
39
- return cls([init_step(step["step"], step.get("settings", {})) for step in config["steps"]])
 
 
 
 
 
40
 
41
  def run(self, collection, start=0, progress=None):
42
  """
@@ -95,7 +101,9 @@ def run_htrflow(custom_template_yaml, batch_image_gallery, progress=gr.Progress(
95
 
96
  pipe = PipelineWithProgress.from_config(config)
97
 
98
- gr.Info(f"HTRflow: processing {len(images)} {'image' if len(images) == 1 else 'images'}.")
 
 
99
  progress(0.1, desc="HTRflow: Processing")
100
 
101
  collection.label = "demo_output"
@@ -192,7 +200,7 @@ def get_image_from_image_id(image_id):
192
  def get_images_from_iiif_manifest(iiif_manifest_url, max_images=20, height=1200):
193
  """
194
  Read images from a v2/v3 IIIF manifest, limited to max_images.
195
-
196
  Arguments:
197
  iiif_manifest_url: URL to IIIF manifest
198
  height: Max height of returned images
@@ -201,7 +209,7 @@ def get_images_from_iiif_manifest(iiif_manifest_url, max_images=20, height=1200)
201
  try:
202
  buffer = io.BytesIO()
203
  c = pycurl.Curl()
204
-
205
  c.setopt(c.URL, iiif_manifest_url)
206
  c.setopt(c.WRITEDATA, buffer)
207
  c.setopt(c.CAINFO, certifi.where())
@@ -211,42 +219,48 @@ def get_images_from_iiif_manifest(iiif_manifest_url, max_images=20, height=1200)
211
  c.setopt(c.TIMEOUT, 10)
212
  c.setopt(c.NOSIGNAL, 1)
213
  c.setopt(c.USERAGENT, "curl/7.68.0")
214
-
215
  c.perform()
216
-
217
  http_code = c.getinfo(c.RESPONSE_CODE)
218
  if http_code != 200:
219
  raise Exception(f"HTTP Error: {http_code}")
220
-
221
  manifest = buffer.getvalue().decode("utf-8")
222
  c.close()
223
-
224
  except pycurl.error as e:
225
  error_code, error_msg = e.args
226
- raise Exception(f"Could not fetch IIIF manifest from {iiif_manifest_url} ({error_msg})")
227
-
 
 
228
  # Hacky solution to get all images regardless of API version - treat
229
  # the manifest as a string and match everything that looks like an IIIF
230
  # image URL.
231
  pattern = r'(?P<identifier>https?://[^"\s]*)/(?P<region>[^"\s]*?)/(?P<size>[^"\s]*?)/(?P<rotation>!?\d*?)/(?P<quality>[^"\s]*?)\.(?P<format>jpg|tif|png|gif|jp2|pdf|webp)'
232
-
233
- images = set() # create a set to eliminate duplicates (e.g. thumbnails and fullsize images)
234
-
 
 
235
  for match in re.findall(pattern, manifest):
236
  identifier, _, _, _, _, format_ = match
237
  images.add(f"{identifier}/full/{height},/0/default.{format_}")
238
-
239
  # Stop adding images if we've reached the maximum
240
  if len(images) >= max_images:
241
  break
242
-
243
  # Sort and limit the results to max_images
244
  return sorted(images)[:max_images], gr.update(visible=True)
245
 
246
 
247
  with gr.Blocks() as submit:
248
  gr.Markdown("# Upload")
249
- gr.Markdown("Select or upload the image you want to transcribe. You can upload up to five images at a time.")
 
 
250
 
251
  collection_submit_state = gr.State()
252
 
@@ -293,11 +307,16 @@ with gr.Blocks() as submit:
293
  "Use an image from a IIIF manifest by pasting a IIIF manifest URL. Press enter to submit."
294
  ),
295
  placeholder="",
296
- scale=0
297
  )
298
- max_images_iiif_manifest= gr.Number(value=20, min_width=50, scale=0,
 
 
 
299
  label="Number of image to return from IIIF manifest",
300
- minimum=1, visible=False)
 
 
301
  iiif_gallery = gr.Gallery(
302
  interactive=False,
303
  columns=4,
@@ -314,6 +333,18 @@ with gr.Blocks() as submit:
314
  placeholder="https://example.com/image.jpg",
315
  )
316
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  with gr.Column(variant="panel", elem_classes="panel-with-border"):
318
  gr.Markdown("## Settings")
319
  gr.Markdown(
@@ -370,7 +401,7 @@ with gr.Blocks() as submit:
370
  )
371
 
372
  with gr.Row():
373
- run_button = gr.Button("Transcribe", variant="primary", scale=0, min_width=200)
374
 
375
  @batch_image_gallery.upload(
376
  inputs=batch_image_gallery,
@@ -385,9 +416,17 @@ with gr.Blocks() as submit:
385
  image_id.submit(get_image_from_image_id, image_id, batch_image_gallery).then(
386
  fn=lambda: "Swedish - Spreads", outputs=pipeline_dropdown
387
  )
388
- iiif_manifest_url.submit(get_images_from_iiif_manifest, [iiif_manifest_url, max_images_iiif_manifest], [iiif_gallery, max_images_iiif_manifest])
 
 
 
 
389
  image_url.submit(lambda url: [url], image_url, batch_image_gallery)
390
 
 
 
 
 
391
  run_button.click(
392
  fn=run_htrflow,
393
  inputs=[custom_template_yaml, batch_image_gallery],
@@ -398,5 +437,6 @@ with gr.Blocks() as submit:
398
  examples.select(get_selected_example_pipeline, None, pipeline_dropdown)
399
 
400
  iiif_gallery.select(get_selected_example_image, None, batch_image_gallery)
 
401
 
402
  edit_pipeline_button.click(lambda: Modal(visible=True), None, edit_pipeline_modal)
 
13
  from htrflow.pipeline.pipeline import Pipeline
14
  from htrflow.pipeline.steps import init_step
15
  from htrflow.volume.volume import Collection
16
+ from pdf2image import convert_from_path
17
 
18
  from app.pipelines import PIPELINES
19
 
 
37
  @classmethod
38
  def from_config(cls, config: dict[str, str]):
39
  """Init pipeline from config, ensuring the correct subclass is instantiated."""
40
+ return cls(
41
+ [
42
+ init_step(step["step"], step.get("settings", {}))
43
+ for step in config["steps"]
44
+ ]
45
+ )
46
 
47
  def run(self, collection, start=0, progress=None):
48
  """
 
101
 
102
  pipe = PipelineWithProgress.from_config(config)
103
 
104
+ gr.Info(
105
+ f"HTRflow: processing {len(images)} {'image' if len(images) == 1 else 'images'}."
106
+ )
107
  progress(0.1, desc="HTRflow: Processing")
108
 
109
  collection.label = "demo_output"
 
200
  def get_images_from_iiif_manifest(iiif_manifest_url, max_images=20, height=1200):
201
  """
202
  Read images from a v2/v3 IIIF manifest, limited to max_images.
203
+
204
  Arguments:
205
  iiif_manifest_url: URL to IIIF manifest
206
  height: Max height of returned images
 
209
  try:
210
  buffer = io.BytesIO()
211
  c = pycurl.Curl()
212
+
213
  c.setopt(c.URL, iiif_manifest_url)
214
  c.setopt(c.WRITEDATA, buffer)
215
  c.setopt(c.CAINFO, certifi.where())
 
219
  c.setopt(c.TIMEOUT, 10)
220
  c.setopt(c.NOSIGNAL, 1)
221
  c.setopt(c.USERAGENT, "curl/7.68.0")
222
+
223
  c.perform()
224
+
225
  http_code = c.getinfo(c.RESPONSE_CODE)
226
  if http_code != 200:
227
  raise Exception(f"HTTP Error: {http_code}")
228
+
229
  manifest = buffer.getvalue().decode("utf-8")
230
  c.close()
231
+
232
  except pycurl.error as e:
233
  error_code, error_msg = e.args
234
+ raise Exception(
235
+ f"Could not fetch IIIF manifest from {iiif_manifest_url} ({error_msg})"
236
+ )
237
+
238
  # Hacky solution to get all images regardless of API version - treat
239
  # the manifest as a string and match everything that looks like an IIIF
240
  # image URL.
241
  pattern = r'(?P<identifier>https?://[^"\s]*)/(?P<region>[^"\s]*?)/(?P<size>[^"\s]*?)/(?P<rotation>!?\d*?)/(?P<quality>[^"\s]*?)\.(?P<format>jpg|tif|png|gif|jp2|pdf|webp)'
242
+
243
+ images = (
244
+ set()
245
+ ) # create a set to eliminate duplicates (e.g. thumbnails and fullsize images)
246
+
247
  for match in re.findall(pattern, manifest):
248
  identifier, _, _, _, _, format_ = match
249
  images.add(f"{identifier}/full/{height},/0/default.{format_}")
250
+
251
  # Stop adding images if we've reached the maximum
252
  if len(images) >= max_images:
253
  break
254
+
255
  # Sort and limit the results to max_images
256
  return sorted(images)[:max_images], gr.update(visible=True)
257
 
258
 
259
  with gr.Blocks() as submit:
260
  gr.Markdown("# Upload")
261
+ gr.Markdown(
262
+ "Select or upload the image you want to transcribe. Most common image formats are supported and you can upload max 5 images at a time in this hosted demo."
263
+ )
264
 
265
  collection_submit_state = gr.State()
266
 
 
307
  "Use an image from a IIIF manifest by pasting a IIIF manifest URL. Press enter to submit."
308
  ),
309
  placeholder="",
310
+ scale=0,
311
  )
312
+ max_images_iiif_manifest = gr.Number(
313
+ value=20,
314
+ min_width=50,
315
+ scale=0,
316
  label="Number of image to return from IIIF manifest",
317
+ minimum=1,
318
+ visible=False,
319
+ )
320
  iiif_gallery = gr.Gallery(
321
  interactive=False,
322
  columns=4,
 
333
  placeholder="https://example.com/image.jpg",
334
  )
335
 
336
+ with gr.Tab("PDF"):
337
+ pdf_file = gr.File(label="PDF", file_types=[".pdf"])
338
+
339
+ pdf_gallery = gr.Gallery(
340
+ interactive=False,
341
+ columns=4,
342
+ allow_preview=False,
343
+ container=False,
344
+ show_label=False,
345
+ object_fit="scale-down",
346
+ )
347
+
348
  with gr.Column(variant="panel", elem_classes="panel-with-border"):
349
  gr.Markdown("## Settings")
350
  gr.Markdown(
 
401
  )
402
 
403
  with gr.Row():
404
+ run_button = gr.Button("Run HTR", variant="primary", scale=0, min_width=200)
405
 
406
  @batch_image_gallery.upload(
407
  inputs=batch_image_gallery,
 
416
  image_id.submit(get_image_from_image_id, image_id, batch_image_gallery).then(
417
  fn=lambda: "Swedish - Spreads", outputs=pipeline_dropdown
418
  )
419
+ iiif_manifest_url.submit(
420
+ get_images_from_iiif_manifest,
421
+ [iiif_manifest_url, max_images_iiif_manifest],
422
+ [iiif_gallery, max_images_iiif_manifest],
423
+ )
424
  image_url.submit(lambda url: [url], image_url, batch_image_gallery)
425
 
426
+ pdf_file.upload(
427
+ lambda imgs: convert_from_path(imgs), inputs=pdf_file, outputs=pdf_gallery
428
+ )
429
+
430
  run_button.click(
431
  fn=run_htrflow,
432
  inputs=[custom_template_yaml, batch_image_gallery],
 
437
  examples.select(get_selected_example_pipeline, None, pipeline_dropdown)
438
 
439
  iiif_gallery.select(get_selected_example_image, None, batch_image_gallery)
440
+ pdf_gallery.select(get_selected_example_image, None, batch_image_gallery)
441
 
442
  edit_pipeline_button.click(lambda: Modal(visible=True), None, edit_pipeline_modal)
pyproject.toml CHANGED
@@ -24,6 +24,7 @@ dependencies = [
24
  "dill>=0.3.9",
25
  "spaces>=0.32.0",
26
  "pycurl",
 
27
  ]
28
 
29
  [project.urls]
 
24
  "dill>=0.3.9",
25
  "spaces>=0.32.0",
26
  "pycurl",
27
+ "pdf2image>=1.17.0",
28
  ]
29
 
30
  [project.urls]
requirements.txt CHANGED
@@ -3,4 +3,5 @@ gradio>=5.20.1
3
  tqdm>=4.67.1
4
  gradio-modal>=0.0.4
5
  dill>=0.3.9
6
- pycurl>=7.45.6
 
 
3
  tqdm>=4.67.1
4
  gradio-modal>=0.0.4
5
  dill>=0.3.9
6
+ pycurl>=7.45.6
7
+ pdf2image>=1.17.0
uv.lock CHANGED
@@ -543,6 +543,8 @@ dependencies = [
543
  { name = "gradio" },
544
  { name = "gradio-modal" },
545
  { name = "htrflow" },
 
 
546
  { name = "spaces" },
547
  { name = "tqdm" },
548
  ]
@@ -560,6 +562,8 @@ requires-dist = [
560
  { name = "gradio", specifier = ">=5.17.0" },
561
  { name = "gradio-modal", specifier = ">=0.0.4" },
562
  { name = "htrflow", specifier = "==0.2.5" },
 
 
563
  { name = "spaces", specifier = ">=0.32.0" },
564
  { name = "tqdm", specifier = ">=4.67.1" },
565
  ]
@@ -1181,6 +1185,18 @@ wheels = [
1181
  { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
1182
  ]
1183
 
 
 
 
 
 
 
 
 
 
 
 
 
1184
  [[package]]
1185
  name = "pfzy"
1186
  version = "0.3.4"
@@ -1357,6 +1373,32 @@ wheels = [
1357
  { url = "https://files.pythonhosted.org/packages/2d/c7/a0d3356f3074ac548afefa515ff46f3bea011deca607faf1c09b26dd5330/pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e", size = 1792099 },
1358
  ]
1359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1360
  [[package]]
1361
  name = "pydantic"
1362
  version = "2.10.6"
 
543
  { name = "gradio" },
544
  { name = "gradio-modal" },
545
  { name = "htrflow" },
546
+ { name = "pdf2image" },
547
+ { name = "pycurl" },
548
  { name = "spaces" },
549
  { name = "tqdm" },
550
  ]
 
562
  { name = "gradio", specifier = ">=5.17.0" },
563
  { name = "gradio-modal", specifier = ">=0.0.4" },
564
  { name = "htrflow", specifier = "==0.2.5" },
565
+ { name = "pdf2image", specifier = ">=1.17.0" },
566
+ { name = "pycurl" },
567
  { name = "spaces", specifier = ">=0.32.0" },
568
  { name = "tqdm", specifier = ">=4.67.1" },
569
  ]
 
1185
  { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
1186
  ]
1187
 
1188
+ [[package]]
1189
+ name = "pdf2image"
1190
+ version = "1.17.0"
1191
+ source = { registry = "https://pypi.org/simple" }
1192
+ dependencies = [
1193
+ { name = "pillow" },
1194
+ ]
1195
+ sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811 }
1196
+ wheels = [
1197
+ { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618 },
1198
+ ]
1199
+
1200
  [[package]]
1201
  name = "pfzy"
1202
  version = "0.3.4"
 
1373
  { url = "https://files.pythonhosted.org/packages/2d/c7/a0d3356f3074ac548afefa515ff46f3bea011deca607faf1c09b26dd5330/pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e", size = 1792099 },
1374
  ]
1375
 
1376
+ [[package]]
1377
+ name = "pycurl"
1378
+ version = "7.45.6"
1379
+ source = { registry = "https://pypi.org/simple" }
1380
+ sdist = { url = "https://files.pythonhosted.org/packages/71/35/fe5088d914905391ef2995102cf5e1892cf32cab1fa6ef8130631c89ec01/pycurl-7.45.6.tar.gz", hash = "sha256:2b73e66b22719ea48ac08a93fc88e57ef36d46d03cb09d972063c9aa86bb74e6", size = 239470 }
1381
+ wheels = [
1382
+ { url = "https://files.pythonhosted.org/packages/31/11/a491677a902053e2720c29e9bc743313e156579da71fcf281b0883e36674/pycurl-7.45.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c31b390f1e2cd4525828f1bb78c1f825c0aab5d1588228ed71b22c4784bdb593", size = 3499748 },
1383
+ { url = "https://files.pythonhosted.org/packages/22/b0/c5d90c97741e9896c76490edead6056adb87d7083e3bf4761218072cf10d/pycurl-7.45.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:942b352b69184cb26920db48e0c5cb95af39874b57dbe27318e60f1e68564e37", size = 3640625 },
1384
+ { url = "https://files.pythonhosted.org/packages/09/63/c7ec2bfedabd71cd71acaed094f3a831ea06198e3b078a199fc353178728/pycurl-7.45.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3441ee77e830267aa6e2bb43b29fd5f8a6bd6122010c76a6f0bf84462e9ea9c7", size = 4879310 },
1385
+ { url = "https://files.pythonhosted.org/packages/ac/b4/1682fe9d8df5223f6dffa34fe99886ec1a02b136d2cea1f5ec4ad46f1548/pycurl-7.45.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2a21e13278d7553a04b421676c458449f6c10509bebf04993f35154b06ee2b20", size = 4561745 },
1386
+ { url = "https://files.pythonhosted.org/packages/be/57/0039590b322ef00a1e5664fab888f26bc18b891d0675340d151a106101ca/pycurl-7.45.6-cp310-cp310-win32.whl", hash = "sha256:d0b5501d527901369aba307354530050f56cd102410f2a3bacd192dc12c645e3", size = 2607483 },
1387
+ { url = "https://files.pythonhosted.org/packages/23/c1/7e6c29a814de032d21fe40e679855a6b359f3b2099ab48f9bef346d88b41/pycurl-7.45.6-cp310-cp310-win_amd64.whl", hash = "sha256:abe1b204a2f96f2eebeaf93411f03505b46d151ef6d9d89326e6dece7b3a008a", size = 3135985 },
1388
+ { url = "https://files.pythonhosted.org/packages/85/45/0cc7060dd0a69dbe4806c71460e7a388e2b19097298eb2b4ac4acddea2a1/pycurl-7.45.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f57ad26d6ab390391ad5030790e3f1a831c1ee54ad3bf969eb378f5957eeb0a", size = 3498594 },
1389
+ { url = "https://files.pythonhosted.org/packages/f4/a9/07a54c81b3fc6d5634e7298f657f9b8fd3d907b1245ef0d16f3b60f67835/pycurl-7.45.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6fd295f03c928da33a00f56c91765195155d2ac6f12878f6e467830b5dce5f5", size = 3640087 },
1390
+ { url = "https://files.pythonhosted.org/packages/59/bc/fb422d2f7568e5ebdbbf34ecfb3e80303d4fa4679319f7d068fb769c521e/pycurl-7.45.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:334721ce1ccd71ff8e405470768b3d221b4393570ccc493fcbdbef4cd62e91ed", size = 4884164 },
1391
+ { url = "https://files.pythonhosted.org/packages/9e/5a/d6708fbf2727a256983513828250c097aff6a99fc222071a030f108f41ba/pycurl-7.45.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0cd6b7794268c17f3c660162ed6381769ce0ad260331ef49191418dfc3a2d61a", size = 4566286 },
1392
+ { url = "https://files.pythonhosted.org/packages/e2/c6/ab4e44ac30853b53239af03eeb4e798851c4bfbe182f3aeec29dc7f11e41/pycurl-7.45.6-cp311-cp311-win32.whl", hash = "sha256:357ea634395310085b9d5116226ac5ec218a6ceebf367c2451ebc8d63a6e9939", size = 2607279 },
1393
+ { url = "https://files.pythonhosted.org/packages/c4/90/b26b72fc8e10cd3ab9fb2cfc995106348e2916779de7feb629772f67d8d2/pycurl-7.45.6-cp311-cp311-win_amd64.whl", hash = "sha256:878ae64484db18f8f10ba99bffc83fefb4fe8f5686448754f93ec32fa4e4ee93", size = 3135613 },
1394
+ { url = "https://files.pythonhosted.org/packages/0a/31/01e3ae56940c9aff2886f06a660641ec779d7265aef8b3bd981add67f3a9/pycurl-7.45.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c872d4074360964697c39c1544fe8c91bfecbff27c1cdda1fee5498e5fdadcda", size = 3499118 },
1395
+ { url = "https://files.pythonhosted.org/packages/02/b2/365423ac0f8d13afffb28e6f3d6bf1aae242934f16ae3eda5f3615c0e978/pycurl-7.45.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56d1197eadd5774582b259cde4364357da71542758d8e917f91cc6ed7ed5b262", size = 3640501 },
1396
+ { url = "https://files.pythonhosted.org/packages/67/d6/7d24b60ee878167e3cef6d37fad7d15cf660c0d510539d1417fd59665d1b/pycurl-7.45.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8a99e56d2575aa74c48c0cd08852a65d5fc952798f76a34236256d5589bf5aa0", size = 4893499 },
1397
+ { url = "https://files.pythonhosted.org/packages/24/8c/c84df085310f2489e293adc7880f82339f50dcd6cf9dba2750b81eaf11d4/pycurl-7.45.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c04230b9e9cfdca9cf3eb09a0bec6cf2f084640f1f1ca1929cca51411af85de2", size = 4576761 },
1398
+ { url = "https://files.pythonhosted.org/packages/bc/a2/682c909d93c27b6c6cd369c5427fee87c525e02c56e1a985c150a2d451fd/pycurl-7.45.6-cp312-cp312-win32.whl", hash = "sha256:ae893144b82d72d95c932ebdeb81fc7e9fde758e5ecd5dd10ad5b67f34a8b8ee", size = 2605115 },
1399
+ { url = "https://files.pythonhosted.org/packages/71/42/b1189c859cec1bdb00c93177e8bdee7c75b23a0e30eba41da637b40444c1/pycurl-7.45.6-cp312-cp312-win_amd64.whl", hash = "sha256:56f841b6f2f7a8b2d3051b9ceebd478599dbea3c8d1de8fb9333c895d0c1eea5", size = 3132702 },
1400
+ ]
1401
+
1402
  [[package]]
1403
  name = "pydantic"
1404
  version = "2.10.6"