nnInteractive 2.3.1__tar.gz → 2.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {nninteractive-2.3.1 → nninteractive-2.3.2}/PKG-INFO +1 -1
  2. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/remote/remote_session.py +34 -3
  3. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/server/app.py +58 -29
  4. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/PKG-INFO +1 -1
  5. {nninteractive-2.3.1 → nninteractive-2.3.2}/pyproject.toml +1 -1
  6. {nninteractive-2.3.1 → nninteractive-2.3.2}/LICENSE +0 -0
  7. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/__init__.py +0 -0
  8. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/__init__.py +0 -0
  9. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/cvpr2025_challenge_baseline/__init__.py +0 -0
  10. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/cvpr2025_challenge_baseline/predict.py +0 -0
  11. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/inference_session.py +0 -0
  12. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/remote/__init__.py +0 -0
  13. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/remote/_protocol.py +0 -0
  14. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/remote/serialization.py +0 -0
  15. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/server/__init__.py +0 -0
  16. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/inference/server/main.py +0 -0
  17. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/interaction/__init__.py +0 -0
  18. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/interaction/point.py +0 -0
  19. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/setup.py +0 -0
  20. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/metadata.py +0 -0
  21. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/reader.py +0 -0
  22. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/run.py +0 -0
  23. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/__init__.py +0 -0
  24. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py +0 -0
  25. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/automatic_mask_generator.py +0 -0
  26. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py +0 -0
  27. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py +0 -0
  28. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/__init__.py +0 -0
  29. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/__init__.py +0 -0
  30. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/hieradet.py +0 -0
  31. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/image_encoder.py +0 -0
  32. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/utils.py +0 -0
  33. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_attention.py +0 -0
  34. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_encoder.py +0 -0
  35. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/position_encoding.py +0 -0
  36. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/__init__.py +0 -0
  37. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/mask_decoder.py +0 -0
  38. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/prompt_encoder.py +0 -0
  39. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/transformer.py +0 -0
  40. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_base.py +0 -0
  41. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_utils.py +0 -0
  42. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_image_predictor.py +0 -0
  43. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor.py +0 -0
  44. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor_legacy.py +0 -0
  45. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py +0 -0
  46. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py +0 -0
  47. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py +0 -0
  48. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/sam2/utils/transforms.py +0 -0
  49. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/setup.py +0 -0
  50. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/__init__.py +0 -0
  51. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/__init__.py +0 -0
  52. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/sam2_datasets.py +0 -0
  53. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/transforms.py +0 -0
  54. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/utils.py +0 -0
  55. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_dataset.py +0 -0
  56. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_raw_dataset.py +0 -0
  57. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_sampler.py +0 -0
  58. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_segment_loader.py +0 -0
  59. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py +0 -0
  60. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/model/__init__.py +0 -0
  61. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py +0 -0
  62. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/optimizer.py +0 -0
  63. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/scripts/sav_frame_extraction_submitit.py +0 -0
  64. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/train.py +0 -0
  65. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/trainer.py +0 -0
  66. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/__init__.py +0 -0
  67. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/checkpoint_utils.py +0 -0
  68. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/data_utils.py +0 -0
  69. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/distributed.py +0 -0
  70. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/logger.py +0 -0
  71. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/sam2/training/utils/train_utils.py +0 -0
  72. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/supervoxel/src/supervoxel.py +0 -0
  73. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/trainer/__init__.py +0 -0
  74. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/trainer/nnInteractiveTrainer.py +0 -0
  75. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/__init__.py +0 -0
  76. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/bboxes.py +0 -0
  77. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/checkpoint_cleansing.py +0 -0
  78. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/crop.py +0 -0
  79. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/erosion_dilation.py +0 -0
  80. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/inference_helpers.py +0 -0
  81. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/os_shennanigans.py +0 -0
  82. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive/utils/rounding.py +0 -0
  83. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/SOURCES.txt +0 -0
  84. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/dependency_links.txt +0 -0
  85. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/entry_points.txt +0 -0
  86. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/requires.txt +0 -0
  87. {nninteractive-2.3.1 → nninteractive-2.3.2}/nnInteractive.egg-info/top_level.txt +0 -0
  88. {nninteractive-2.3.1 → nninteractive-2.3.2}/readme.md +0 -0
  89. {nninteractive-2.3.1 → nninteractive-2.3.2}/setup.cfg +0 -0
  90. {nninteractive-2.3.1 → nninteractive-2.3.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnInteractive
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Inference code for nnInteractive
5
5
  Author: Helmholtz Imaging Applied Computer Vision Lab
6
6
  Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
@@ -132,6 +132,13 @@ class nnInteractiveRemoteInferenceSession:
132
132
  the server before the client gives up. Default 60s matches observed
133
133
  prediction times (100ms..~10s) with headroom for slow links. On
134
134
  expiry: ``httpx.ReadTimeout``.
135
+ set_image_read_timeout:
136
+ Read timeout (seconds) used *only* for ``set_image``. After the volume
137
+ is uploaded, the server decompresses and preprocesses the full image
138
+ before responding, which can take far longer than a prediction on a
139
+ large volume. ``set_image`` therefore gets its own generous read
140
+ timeout instead of the much tighter ``read_timeout`` used for
141
+ predictions. On expiry: ``httpx.ReadTimeout``.
135
142
  write_timeout:
136
143
  Seconds to finish uploading the request body. ``set_image`` uploads
137
144
  the full 4D volume so this is the longest-running upload. On expiry:
@@ -146,6 +153,7 @@ class nnInteractiveRemoteInferenceSession:
146
153
  api_key: Optional[str] = None,
147
154
  connect_timeout: float = 10.0,
148
155
  read_timeout: float = 60.0,
156
+ set_image_read_timeout: float = 600.0,
149
157
  write_timeout: float = 120.0,
150
158
  pool_timeout: float = 10.0,
151
159
  ):
@@ -166,6 +174,15 @@ class nnInteractiveRemoteInferenceSession:
166
174
  ),
167
175
  headers=headers,
168
176
  )
177
+ # Per-request timeout override for set_image: same connect/write/pool as
178
+ # the client default, but a much longer read budget for server-side
179
+ # decompression + preprocessing of the full volume.
180
+ self._set_image_timeout = httpx.Timeout(
181
+ connect=connect_timeout,
182
+ read=set_image_read_timeout,
183
+ write=write_timeout,
184
+ pool=pool_timeout,
185
+ )
169
186
  self._lease_token: Optional[str] = None
170
187
 
171
188
  # Claim a session on the server. The lease token is then attached to
@@ -242,12 +259,21 @@ class nnInteractiveRemoteInferenceSession:
242
259
  resp.raise_for_status()
243
260
  return resp
244
261
 
245
- def _post_binary(self, path: str, meta: dict, array_bytes: bytes) -> httpx.Response:
262
+ def _post_binary(
263
+ self,
264
+ path: str,
265
+ meta: dict,
266
+ array_bytes: bytes,
267
+ timeout: Union[httpx.Timeout, float, None] = None,
268
+ ) -> httpx.Response:
246
269
  headers = {
247
270
  META_HEADER: json.dumps(_to_jsonable(meta), separators=(",", ":")),
248
271
  "Content-Type": CONTENT_TYPE_OCTET_STREAM,
249
272
  }
250
- resp = self._http.post(path, content=array_bytes, headers=headers)
273
+ # httpx treats timeout=None as "no override" only when the arg is
274
+ # omitted; pass it through explicitly only when a caller supplied one.
275
+ kwargs = {} if timeout is None else {"timeout": timeout}
276
+ resp = self._http.post(path, content=array_bytes, headers=headers, **kwargs)
251
277
  _raise_for_lease_errors(resp)
252
278
  resp.raise_for_status()
253
279
  return resp
@@ -300,7 +326,12 @@ class nnInteractiveRemoteInferenceSession:
300
326
  def set_image(self, image: np.ndarray, image_properties: Optional[dict] = None) -> None:
301
327
  assert image.ndim == 4, f"expected a 4d image as input, got {image.ndim}d. Shape {image.shape}"
302
328
  meta = {"image_properties": image_properties or {}}
303
- resp = self._post_binary(PATH_SET_IMAGE, meta, pack_array(image, nthreads=_compression_threads()))
329
+ resp = self._post_binary(
330
+ PATH_SET_IMAGE,
331
+ meta,
332
+ pack_array(image, nthreads=_compression_threads()),
333
+ timeout=self._set_image_timeout,
334
+ )
304
335
  info = resp.json()
305
336
  self.original_image_shape = tuple(info["original_image_shape"])
306
337
 
@@ -31,6 +31,15 @@ Concurrency model:
31
31
  prediction runs at a time.
32
32
  - The acquisition order is always (session lock → gpu lock) so there is no
33
33
  deadlock potential.
34
+ - The endpoints that carry large payloads (``set_image`` and the mask
35
+ interactions) are ``async`` so they can ``await`` the upload, but their
36
+ CPU-bound work (blosc2 decompression, image preprocessing, prediction,
37
+ response compression) is dispatched to a worker thread via
38
+ ``run_in_threadpool``. This keeps the event loop free during a long
39
+ ``set_image``/predict so lightweight endpoints — ``/heartbeat``,
40
+ ``/healthz`` — and the background reaper stay responsive, and so two
41
+ clients can genuinely preprocess concurrently. Acquiring a session/gpu
42
+ lock therefore also happens off the loop, never stalling it.
34
43
  """
35
44
 
36
45
  from __future__ import annotations
@@ -50,6 +59,7 @@ import blosc2
50
59
  import numpy as np
51
60
  import torch
52
61
  from fastapi import Depends, FastAPI, HTTPException, Header, Request, Response, status
62
+ from starlette.concurrency import run_in_threadpool
53
63
 
54
64
  from nnInteractive.inference.inference_session import nnInteractiveInferenceSession
55
65
  from nnInteractive.inference.remote._protocol import (
@@ -570,17 +580,24 @@ def make_app(
570
580
  async def set_image(request: Request, entry: SessionEntry = lease) -> dict:
571
581
  meta = _parse_meta_header(request.headers.get(META_HEADER))
572
582
  body = await request.body()
573
- image = unpack_array(body)
574
583
  image_properties = meta.get("image_properties") or {}
575
584
 
576
- def _do(session):
577
- session.set_image(image, image_properties)
578
- # set_image preprocesses in a background thread; force completion so
579
- # subsequent calls can safely use original_image_shape.
580
- session._finish_preprocessing_and_initialize_interactions()
581
- return {"original_image_shape": list(session.original_image_shape)}
585
+ # Decompression + full-volume preprocessing are CPU-bound and can run
586
+ # for many seconds on a large image. Run them in a worker thread so the
587
+ # event loop keeps servicing heartbeats/healthz and the reaper.
588
+ def _work():
589
+ image = unpack_array(body)
582
590
 
583
- return _under_session_lock(entry, _do)
591
+ def _do(session):
592
+ session.set_image(image, image_properties)
593
+ # set_image preprocesses in a background thread; force completion
594
+ # so subsequent calls can safely use original_image_shape.
595
+ session._finish_preprocessing_and_initialize_interactions()
596
+ return {"original_image_shape": list(session.original_image_shape)}
597
+
598
+ return _under_session_lock(entry, _do)
599
+
600
+ return await run_in_threadpool(_work)
584
601
 
585
602
  @app.post(PATH_SET_TARGET_BUFFER, dependencies=[auth])
586
603
  def set_target_buffer(payload: dict, entry: SessionEntry = lease) -> dict:
@@ -652,41 +669,53 @@ def make_app(
652
669
  async def _handle_mask_interaction(request: Request, entry: SessionEntry, kind: str) -> Response:
653
670
  meta = _parse_meta_header(request.headers.get(META_HEADER))
654
671
  body = await request.body()
655
- mask = unpack_array(body)
656
672
  run_prediction = bool(meta.get("run_prediction", True))
657
673
  interaction_bbox = meta.get("interaction_bbox")
658
674
  if interaction_bbox is not None:
659
675
  interaction_bbox = [list(b) for b in interaction_bbox]
660
676
 
661
- def _do(session):
662
- method = session.add_scribble_interaction if kind == "scribble" else session.add_lasso_interaction
663
- method(
664
- mask,
665
- bool(meta["include_interaction"]),
666
- run_prediction=run_prediction,
667
- override_capability_checks=bool(meta.get("override_capability_checks", False)),
668
- interaction_bbox=interaction_bbox,
669
- )
670
- return _build_prediction_response(session, run_prediction)
677
+ # Decompression + prediction + response compression are CPU/GPU-bound;
678
+ # run them off the event loop (see set_image).
679
+ def _work():
680
+ mask = unpack_array(body)
671
681
 
672
- return _under_session_and_gpu_lock(entry, _do)
682
+ def _do(session):
683
+ method = session.add_scribble_interaction if kind == "scribble" else session.add_lasso_interaction
684
+ method(
685
+ mask,
686
+ bool(meta["include_interaction"]),
687
+ run_prediction=run_prediction,
688
+ override_capability_checks=bool(meta.get("override_capability_checks", False)),
689
+ interaction_bbox=interaction_bbox,
690
+ )
691
+ return _build_prediction_response(session, run_prediction)
692
+
693
+ return _under_session_and_gpu_lock(entry, _do)
694
+
695
+ return await run_in_threadpool(_work)
673
696
 
674
697
  @app.post(PATH_ADD_INITIAL_SEG, dependencies=[auth])
675
698
  async def add_initial_seg_interaction(request: Request, entry: SessionEntry = lease) -> Response:
676
699
  meta = _parse_meta_header(request.headers.get(META_HEADER))
677
700
  body = await request.body()
678
- initial_seg = unpack_array(body)
679
701
  run_prediction = bool(meta.get("run_prediction", False))
680
702
 
681
- def _do(session):
682
- session.add_initial_seg_interaction(
683
- initial_seg=initial_seg,
684
- run_prediction=run_prediction,
685
- override_capability_checks=bool(meta.get("override_capability_checks", False)),
686
- )
687
- return _build_prediction_response(session, run_prediction)
703
+ # Decompression + (optional) prediction are CPU/GPU-bound; run them off
704
+ # the event loop (see set_image).
705
+ def _work():
706
+ initial_seg = unpack_array(body)
688
707
 
689
- return _under_session_and_gpu_lock(entry, _do)
708
+ def _do(session):
709
+ session.add_initial_seg_interaction(
710
+ initial_seg=initial_seg,
711
+ run_prediction=run_prediction,
712
+ override_capability_checks=bool(meta.get("override_capability_checks", False)),
713
+ )
714
+ return _build_prediction_response(session, run_prediction)
715
+
716
+ return _under_session_and_gpu_lock(entry, _do)
717
+
718
+ return await run_in_threadpool(_work)
690
719
 
691
720
  return app
692
721
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnInteractive
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Inference code for nnInteractive
5
5
  Author: Helmholtz Imaging Applied Computer Vision Lab
6
6
  Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nnInteractive"
3
- version = "2.3.1"
3
+ version = "2.3.2"
4
4
  requires-python = ">=3.10"
5
5
  description = "Inference code for nnInteractive"
6
6
  readme = "readme.md"
File without changes
File without changes
File without changes
File without changes