novelai-python 0.2.9__tar.gz → 0.3.1__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.
- {novelai_python-0.2.9 → novelai_python-0.3.1}/PKG-INFO +2 -1
- {novelai_python-0.2.9 → novelai_python-0.3.1}/pyproject.toml +2 -1
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/ai/generate_image.py +17 -12
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/server.py +29 -1
- novelai_python-0.3.1/src/novelai_python/utils/useful.py +104 -0
- novelai_python-0.2.9/src/novelai_python/utils/useful.py +0 -44
- {novelai_python-0.2.9 → novelai_python-0.3.1}/LICENSE +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/NOTICE.MD +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/README.md +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/credential/JwtToken.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/credential/UserAuth.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/ai/upscale.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/user/information.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/user/login.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/sdk/user/subscription.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/hash.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag_artist.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag_character.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/__init__.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_server.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_server_run.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_upscale.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_user_information.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_user_login.py +0 -0
- {novelai_python-0.2.9 → novelai_python-0.3.1}/tests/test_user_subscription.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: novelai-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Novelai Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -19,6 +19,7 @@ Requires-Dist: fastapi>=0.109.0
|
|
|
19
19
|
Requires-Dist: uvicorn[standard]>=0.27.0.post1
|
|
20
20
|
Requires-Dist: numpy>=1.24.4
|
|
21
21
|
Requires-Dist: argon2-cffi>=23.1.0
|
|
22
|
+
Requires-Dist: opencv-python>=4.8.1.78
|
|
22
23
|
Requires-Dist: pytest>=7.4.4; extra == "testing"
|
|
23
24
|
Requires-Dist: pytest-asyncio>=0.23.4; extra == "testing"
|
|
24
25
|
Provides-Extra: testing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "novelai-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Novelai Python Binding With Pydantic"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "sudoskys", email = "coldlando@hotmail.com" },
|
|
@@ -18,6 +18,7 @@ dependencies = [
|
|
|
18
18
|
"uvicorn[standard]>=0.27.0.post1",
|
|
19
19
|
"numpy>=1.24.4",
|
|
20
20
|
"argon2-cffi>=23.1.0",
|
|
21
|
+
"opencv-python>=4.8.1.78",
|
|
21
22
|
]
|
|
22
23
|
requires-python = ">=3.8"
|
|
23
24
|
readme = "README.md"
|
|
@@ -7,6 +7,7 @@ import base64
|
|
|
7
7
|
import json
|
|
8
8
|
import math
|
|
9
9
|
import random
|
|
10
|
+
from copy import deepcopy
|
|
10
11
|
from enum import Enum, IntEnum
|
|
11
12
|
from io import BytesIO
|
|
12
13
|
from typing import Optional, Union
|
|
@@ -207,6 +208,14 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
207
208
|
parameters: Params = Params()
|
|
208
209
|
model_config = ConfigDict(extra="ignore")
|
|
209
210
|
|
|
211
|
+
@property
|
|
212
|
+
def endpoint(self):
|
|
213
|
+
return self._endpoint
|
|
214
|
+
|
|
215
|
+
@endpoint.setter
|
|
216
|
+
def endpoint(self, value):
|
|
217
|
+
self._endpoint = value
|
|
218
|
+
|
|
210
219
|
@override
|
|
211
220
|
def model_post_init(self, *args) -> None:
|
|
212
221
|
"""
|
|
@@ -241,20 +250,14 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
241
250
|
def validate_model(self):
|
|
242
251
|
if self.action == Action.INFILL and not self.parameters.mask:
|
|
243
252
|
logger.warning("Mask maybe required for infill mode.")
|
|
253
|
+
if self.action != Action.GENERATE:
|
|
254
|
+
self.parameters.extra_noise_seed = self.parameters.seed
|
|
244
255
|
return self
|
|
245
256
|
|
|
246
257
|
@property
|
|
247
258
|
def base_url(self):
|
|
248
259
|
return f"{self.endpoint.strip('/')}/ai/generate-image"
|
|
249
260
|
|
|
250
|
-
@property
|
|
251
|
-
def endpoint(self):
|
|
252
|
-
return self._endpoint
|
|
253
|
-
|
|
254
|
-
@endpoint.setter
|
|
255
|
-
def endpoint(self, value):
|
|
256
|
-
self._endpoint = value
|
|
257
|
-
|
|
258
261
|
def calculate_cost(self, is_opus: bool = False):
|
|
259
262
|
"""
|
|
260
263
|
Calculate the Anlas cost of current parameters.
|
|
@@ -392,6 +395,7 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
392
395
|
"Sec-Fetch-Site": "same-site",
|
|
393
396
|
"Pragma": "no-cache",
|
|
394
397
|
"Cache-Control": "no-cache",
|
|
398
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0"
|
|
395
399
|
}
|
|
396
400
|
|
|
397
401
|
async def request(self,
|
|
@@ -419,20 +423,21 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
419
423
|
session.headers.clear()
|
|
420
424
|
session.headers.update(override_headers)
|
|
421
425
|
try:
|
|
422
|
-
_log_data = request_data
|
|
426
|
+
_log_data = deepcopy(request_data)
|
|
423
427
|
if self.action == Action.GENERATE:
|
|
424
428
|
logger.debug(f"Request Data: {_log_data}")
|
|
425
429
|
else:
|
|
426
430
|
_log_data.get("parameters", {}).update({
|
|
427
|
-
"image": "base64 data" if self.parameters.image else
|
|
431
|
+
"image": "base64 data" if self.parameters.image else None,
|
|
428
432
|
}
|
|
429
433
|
)
|
|
430
434
|
_log_data.get("parameters", {}).update(
|
|
431
435
|
{
|
|
432
|
-
"mask": "base64 data" if self.parameters.mask else
|
|
436
|
+
"mask": "base64 data" if self.parameters.mask else None,
|
|
433
437
|
}
|
|
434
438
|
)
|
|
435
|
-
logger.debug(f"Request Data: {
|
|
439
|
+
logger.debug(f"Request Data: {_log_data}")
|
|
440
|
+
del _log_data
|
|
436
441
|
except Exception as e:
|
|
437
442
|
logger.warning(f"Error when print log data: {e}")
|
|
438
443
|
try:
|
|
@@ -15,6 +15,7 @@ from starlette.responses import JSONResponse, StreamingResponse
|
|
|
15
15
|
|
|
16
16
|
from .credential import JwtCredential, SecretStr
|
|
17
17
|
from .sdk.ai.generate_image import GenerateImageInfer
|
|
18
|
+
from .sdk.ai.upscale import Upscale
|
|
18
19
|
from .sdk.user.information import Information
|
|
19
20
|
from .sdk.user.login import Login
|
|
20
21
|
from .sdk.user.subscription import Subscription
|
|
@@ -90,6 +91,33 @@ async def subscription(
|
|
|
90
91
|
return JSONResponse(status_code=500, content=e.__dict__)
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
@app.post("/ai/upscale")
|
|
95
|
+
async def upscale(
|
|
96
|
+
req: Upscale,
|
|
97
|
+
current_token: str = Depends(get_current_token)
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
生成图片
|
|
101
|
+
:param current_token: Authorization
|
|
102
|
+
:param req: Upscale
|
|
103
|
+
:return:
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
_result = await req.request(session=get_session(current_token), remove_sign=True)
|
|
107
|
+
zip_file_bytes = io.BytesIO()
|
|
108
|
+
with zipfile.ZipFile(zip_file_bytes, mode="w", compression=zipfile.ZIP_DEFLATED) as zip_file:
|
|
109
|
+
file = _result.files # ONLY TUPLE
|
|
110
|
+
zip_file.writestr(zinfo_or_arcname=file[0], data=file[1])
|
|
111
|
+
# return the zip file
|
|
112
|
+
zip_file_bytes.seek(0)
|
|
113
|
+
return StreamingResponse(zip_file_bytes, media_type='application/zip', headers={
|
|
114
|
+
'Content-Disposition': 'attachment;filename=image.zip'
|
|
115
|
+
})
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.exception(e)
|
|
118
|
+
return JSONResponse(status_code=500, content=e.__dict__)
|
|
119
|
+
|
|
120
|
+
|
|
93
121
|
@app.post("/ai/generate_image")
|
|
94
122
|
async def generate_image(
|
|
95
123
|
req: GenerateImageInfer,
|
|
@@ -110,7 +138,7 @@ async def generate_image(
|
|
|
110
138
|
# return the zip file
|
|
111
139
|
zip_file_bytes.seek(0)
|
|
112
140
|
return StreamingResponse(zip_file_bytes, media_type='application/zip', headers={
|
|
113
|
-
'Content-Disposition': 'attachment;filename=
|
|
141
|
+
'Content-Disposition': 'attachment;filename=image.zip'
|
|
114
142
|
})
|
|
115
143
|
except Exception as e:
|
|
116
144
|
logger.exception(e)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/2/9 下午10:41
|
|
3
|
+
# @Author : sudoskys
|
|
4
|
+
# @File : useful.py
|
|
5
|
+
# @Software: PyCharm
|
|
6
|
+
import collections
|
|
7
|
+
import random
|
|
8
|
+
from typing import List, Union
|
|
9
|
+
|
|
10
|
+
import cv2 as cv
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def enum_to_list(enum_):
|
|
15
|
+
return list(map(lambda x: x.value, enum_._member_map_.values()))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueSelect(object):
|
|
19
|
+
def __init__(self, data: List[str]):
|
|
20
|
+
"""
|
|
21
|
+
A queue selector
|
|
22
|
+
:param data:
|
|
23
|
+
"""
|
|
24
|
+
self.data = collections.deque(data)
|
|
25
|
+
self.used = collections.deque()
|
|
26
|
+
self.users = {}
|
|
27
|
+
|
|
28
|
+
def get(self, user_id: Union[int, str]) -> str:
|
|
29
|
+
user_id = str(user_id)
|
|
30
|
+
if user_id not in self.users:
|
|
31
|
+
self.users[user_id] = {'data': self.data.copy(), 'used': collections.deque()}
|
|
32
|
+
|
|
33
|
+
user_data = self.users[user_id]['data']
|
|
34
|
+
user_used = self.users[user_id]['used']
|
|
35
|
+
|
|
36
|
+
if len(user_data) == 0:
|
|
37
|
+
user_data, user_used = user_used, user_data
|
|
38
|
+
# 随机掉 User data
|
|
39
|
+
if len(user_data) > 2:
|
|
40
|
+
random.shuffle(user_data)
|
|
41
|
+
self.users[user_id]['data'] = user_data
|
|
42
|
+
self.users[user_id]['used'] = user_used
|
|
43
|
+
|
|
44
|
+
selected = user_data.popleft()
|
|
45
|
+
user_used.append(selected)
|
|
46
|
+
|
|
47
|
+
return selected
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_mask_from_sketch(original_img_bytes: bytes,
|
|
51
|
+
sketch_img_bytes: bytes,
|
|
52
|
+
output_format: str = '.png',
|
|
53
|
+
jagged_edges: bool = True
|
|
54
|
+
) -> bytes:
|
|
55
|
+
"""
|
|
56
|
+
Function to create a mask from original and sketch images input as bytes. Returns bytes.
|
|
57
|
+
|
|
58
|
+
:param jagged_edges: Whether to create jagged edges in the mask. Defaults to False.
|
|
59
|
+
:param original_img_bytes: Bytes corresponding to the original image.
|
|
60
|
+
:param sketch_img_bytes: Bytes corresponding to the sketch image.
|
|
61
|
+
:param output_format: Format of the output image. Defaults to '.png'. It could also be '.jpg'
|
|
62
|
+
:returns bytes: Bytes corresponding to the resultant mask
|
|
63
|
+
"""
|
|
64
|
+
# Load images
|
|
65
|
+
ori_img = cv.imdecode(np.frombuffer(original_img_bytes, np.uint8), cv.IMREAD_COLOR)
|
|
66
|
+
sketch_img = cv.imdecode(np.frombuffer(sketch_img_bytes, np.uint8), cv.IMREAD_COLOR)
|
|
67
|
+
|
|
68
|
+
# Check if images have the same size
|
|
69
|
+
if ori_img.shape != sketch_img.shape:
|
|
70
|
+
raise ValueError("Images must have the same size.")
|
|
71
|
+
|
|
72
|
+
# Calculate difference between the original and sketch images
|
|
73
|
+
diff_img = cv.absdiff(ori_img, sketch_img)
|
|
74
|
+
|
|
75
|
+
# Convert the difference image to grayscale
|
|
76
|
+
diff_gray = cv.cvtColor(diff_img, cv.COLOR_BGR2GRAY)
|
|
77
|
+
|
|
78
|
+
# Threshold to create the mask
|
|
79
|
+
_, thresh = cv.threshold(diff_gray, 10, 255, cv.THRESH_BINARY)
|
|
80
|
+
|
|
81
|
+
if jagged_edges:
|
|
82
|
+
# Use a bigger kernel for dilation to create larger 'step' effect at the edges
|
|
83
|
+
kernel = np.ones((7, 7), np.uint8)
|
|
84
|
+
else:
|
|
85
|
+
kernel = np.ones((3, 3), np.uint8)
|
|
86
|
+
|
|
87
|
+
# Perform morphological opening to remove small noise
|
|
88
|
+
opening = cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations=2)
|
|
89
|
+
|
|
90
|
+
# Further remove noise with a Gaussian filter
|
|
91
|
+
smooth = cv.GaussianBlur(opening, (5, 5), 0)
|
|
92
|
+
|
|
93
|
+
if jagged_edges:
|
|
94
|
+
# Apply additional thresholding to create sharper, jagged edges
|
|
95
|
+
_, smooth = cv.threshold(smooth, 128, 255, cv.THRESH_BINARY)
|
|
96
|
+
|
|
97
|
+
# Convert image to BytesIO object
|
|
98
|
+
is_success, buffer = cv.imencode(output_format, smooth)
|
|
99
|
+
if is_success:
|
|
100
|
+
output_io = buffer.tobytes()
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError("Error during conversion of image to BytesIO object")
|
|
103
|
+
|
|
104
|
+
return output_io
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
# @Time : 2024/2/9 下午10:41
|
|
3
|
-
# @Author : sudoskys
|
|
4
|
-
# @File : useful.py
|
|
5
|
-
# @Software: PyCharm
|
|
6
|
-
import collections
|
|
7
|
-
import random
|
|
8
|
-
from typing import List, Union
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def enum_to_list(enum_):
|
|
12
|
-
return list(map(lambda x: x.value, enum_._member_map_.values()))
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class QueSelect(object):
|
|
16
|
-
def __init__(self, data: List[str]):
|
|
17
|
-
"""
|
|
18
|
-
A queue selector
|
|
19
|
-
:param data:
|
|
20
|
-
"""
|
|
21
|
-
self.data = collections.deque(data)
|
|
22
|
-
self.used = collections.deque()
|
|
23
|
-
self.users = {}
|
|
24
|
-
|
|
25
|
-
def get(self, user_id: Union[int, str]) -> str:
|
|
26
|
-
user_id = str(user_id)
|
|
27
|
-
if user_id not in self.users:
|
|
28
|
-
self.users[user_id] = {'data': self.data.copy(), 'used': collections.deque()}
|
|
29
|
-
|
|
30
|
-
user_data = self.users[user_id]['data']
|
|
31
|
-
user_used = self.users[user_id]['used']
|
|
32
|
-
|
|
33
|
-
if len(user_data) == 0:
|
|
34
|
-
user_data, user_used = user_used, user_data
|
|
35
|
-
# 随机掉 User data
|
|
36
|
-
if len(user_data) > 2:
|
|
37
|
-
random.shuffle(user_data)
|
|
38
|
-
self.users[user_id]['data'] = user_data
|
|
39
|
-
self.users[user_id]['used'] = user_used
|
|
40
|
-
|
|
41
|
-
selected = user_data.popleft()
|
|
42
|
-
user_used.append(selected)
|
|
43
|
-
|
|
44
|
-
return selected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/information.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/_response/user/subscription.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag_artist.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.2.9 → novelai_python-0.3.1}/src/novelai_python/utils/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|