|
import os |
|
from folder_paths import get_output_directory |
|
from comfy_api_nodes.mapper_utils import model_field_to_node_input |
|
from comfy.comfy_types.node_typing import IO |
|
from comfy_api_nodes.apis import ( |
|
TripoOrientation, |
|
TripoModelVersion, |
|
) |
|
from comfy_api_nodes.apis.tripo_api import ( |
|
TripoTaskType, |
|
TripoStyle, |
|
TripoFileReference, |
|
TripoFileEmptyReference, |
|
TripoUrlReference, |
|
TripoTaskResponse, |
|
TripoTaskStatus, |
|
TripoTextToModelRequest, |
|
TripoImageToModelRequest, |
|
TripoMultiviewToModelRequest, |
|
TripoTextureModelRequest, |
|
TripoRefineModelRequest, |
|
TripoAnimateRigRequest, |
|
TripoAnimateRetargetRequest, |
|
TripoConvertModelRequest, |
|
) |
|
|
|
from comfy_api_nodes.apis.client import ( |
|
ApiEndpoint, |
|
HttpMethod, |
|
SynchronousOperation, |
|
PollingOperation, |
|
EmptyRequest, |
|
) |
|
from comfy_api_nodes.apinode_utils import ( |
|
upload_images_to_comfyapi, |
|
download_url_to_bytesio, |
|
) |
|
|
|
|
|
async def upload_image_to_tripo(image, **kwargs): |
|
urls = await upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs) |
|
return TripoFileReference(TripoUrlReference(url=urls[0], type="jpeg")) |
|
|
|
def get_model_url_from_response(response: TripoTaskResponse) -> str: |
|
if response.data is not None: |
|
for key in ["pbr_model", "model", "base_model"]: |
|
if getattr(response.data.output, key, None) is not None: |
|
return getattr(response.data.output, key) |
|
raise RuntimeError(f"Failed to get model url from response: {response}") |
|
|
|
|
|
async def poll_until_finished( |
|
kwargs: dict[str, str], |
|
response: TripoTaskResponse, |
|
) -> tuple[str, str]: |
|
"""Polls the Tripo API endpoint until the task reaches a terminal state, then returns the response.""" |
|
if response.code != 0: |
|
raise RuntimeError(f"Failed to generate mesh: {response.error}") |
|
task_id = response.data.task_id |
|
response_poll = await PollingOperation( |
|
poll_endpoint=ApiEndpoint( |
|
path=f"/proxy/tripo/v2/openapi/task/{task_id}", |
|
method=HttpMethod.GET, |
|
request_model=EmptyRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
completed_statuses=[TripoTaskStatus.SUCCESS], |
|
failed_statuses=[ |
|
TripoTaskStatus.FAILED, |
|
TripoTaskStatus.CANCELLED, |
|
TripoTaskStatus.UNKNOWN, |
|
TripoTaskStatus.BANNED, |
|
TripoTaskStatus.EXPIRED, |
|
], |
|
status_extractor=lambda x: x.data.status, |
|
auth_kwargs=kwargs, |
|
node_id=kwargs["unique_id"], |
|
result_url_extractor=get_model_url_from_response, |
|
progress_extractor=lambda x: x.data.progress, |
|
).execute() |
|
if response_poll.data.status == TripoTaskStatus.SUCCESS: |
|
url = get_model_url_from_response(response_poll) |
|
bytesio = await download_url_to_bytesio(url) |
|
|
|
model_file = f"tripo_model_{task_id}.glb" |
|
with open(os.path.join(get_output_directory(), model_file), "wb") as f: |
|
f.write(bytesio.getvalue()) |
|
return model_file, task_id |
|
raise RuntimeError(f"Failed to generate mesh: {response_poll}") |
|
|
|
|
|
class TripoTextToModelNode: |
|
""" |
|
Generates 3D models synchronously based on a text prompt using Tripo's API. |
|
""" |
|
AVERAGE_DURATION = 80 |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"prompt": ("STRING", {"multiline": True}), |
|
}, |
|
"optional": { |
|
"negative_prompt": ("STRING", {"multiline": True}), |
|
"model_version": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "model_version", enum_type=TripoModelVersion), |
|
"style": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "style", enum_type=TripoStyle, default="None"), |
|
"texture": ("BOOLEAN", {"default": True}), |
|
"pbr": ("BOOLEAN", {"default": True}), |
|
"image_seed": ("INT", {"default": 42}), |
|
"model_seed": ("INT", {"default": 42}), |
|
"texture_seed": ("INT", {"default": 42}), |
|
"texture_quality": (["standard", "detailed"], {"default": "standard"}), |
|
"face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), |
|
"quad": ("BOOLEAN", {"default": False}) |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) |
|
RETURN_NAMES = ("model_file", "model task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
|
|
async def generate_mesh(self, prompt, negative_prompt=None, model_version=None, style=None, texture=None, pbr=None, image_seed=None, model_seed=None, texture_seed=None, texture_quality=None, face_limit=None, quad=None, **kwargs): |
|
style_enum = None if style == "None" else style |
|
if not prompt: |
|
raise RuntimeError("Prompt is required") |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoTextToModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoTextToModelRequest( |
|
type=TripoTaskType.TEXT_TO_MODEL, |
|
prompt=prompt, |
|
negative_prompt=negative_prompt if negative_prompt else None, |
|
model_version=model_version, |
|
style=style_enum, |
|
texture=texture, |
|
pbr=pbr, |
|
image_seed=image_seed, |
|
model_seed=model_seed, |
|
texture_seed=texture_seed, |
|
texture_quality=texture_quality, |
|
face_limit=face_limit, |
|
auto_size=True, |
|
quad=quad |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoImageToModelNode: |
|
""" |
|
Generates 3D models synchronously based on a single image using Tripo's API. |
|
""" |
|
AVERAGE_DURATION = 80 |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"image": ("IMAGE",), |
|
}, |
|
"optional": { |
|
"model_version": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "model_version", enum_type=TripoModelVersion), |
|
"style": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "style", enum_type=TripoStyle, default="None"), |
|
"texture": ("BOOLEAN", {"default": True}), |
|
"pbr": ("BOOLEAN", {"default": True}), |
|
"model_seed": ("INT", {"default": 42}), |
|
"orientation": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "orientation", enum_type=TripoOrientation), |
|
"texture_seed": ("INT", {"default": 42}), |
|
"texture_quality": (["standard", "detailed"], {"default": "standard"}), |
|
"texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), |
|
"face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), |
|
"quad": ("BOOLEAN", {"default": False}) |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) |
|
RETURN_NAMES = ("model_file", "model task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
|
|
async def generate_mesh(self, image, model_version=None, style=None, texture=None, pbr=None, model_seed=None, orientation=None, texture_alignment=None, texture_seed=None, texture_quality=None, face_limit=None, quad=None, **kwargs): |
|
style_enum = None if style == "None" else style |
|
if image is None: |
|
raise RuntimeError("Image is required") |
|
tripo_file = await upload_image_to_tripo(image, **kwargs) |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoImageToModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoImageToModelRequest( |
|
type=TripoTaskType.IMAGE_TO_MODEL, |
|
file=tripo_file, |
|
model_version=model_version, |
|
style=style_enum, |
|
texture=texture, |
|
pbr=pbr, |
|
model_seed=model_seed, |
|
orientation=orientation, |
|
texture_alignment=texture_alignment, |
|
texture_seed=texture_seed, |
|
texture_quality=texture_quality, |
|
face_limit=face_limit, |
|
auto_size=True, |
|
quad=quad |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoMultiviewToModelNode: |
|
""" |
|
Generates 3D models synchronously based on up to four images (front, left, back, right) using Tripo's API. |
|
""" |
|
AVERAGE_DURATION = 80 |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"image": ("IMAGE",), |
|
}, |
|
"optional": { |
|
"image_left": ("IMAGE",), |
|
"image_back": ("IMAGE",), |
|
"image_right": ("IMAGE",), |
|
"model_version": model_field_to_node_input(IO.COMBO, TripoMultiviewToModelRequest, "model_version", enum_type=TripoModelVersion), |
|
"orientation": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "orientation", enum_type=TripoOrientation), |
|
"texture": ("BOOLEAN", {"default": True}), |
|
"pbr": ("BOOLEAN", {"default": True}), |
|
"model_seed": ("INT", {"default": 42}), |
|
"texture_seed": ("INT", {"default": 42}), |
|
"texture_quality": (["standard", "detailed"], {"default": "standard"}), |
|
"texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), |
|
"face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), |
|
"quad": ("BOOLEAN", {"default": False}) |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) |
|
RETURN_NAMES = ("model_file", "model task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
|
|
async def generate_mesh(self, image, image_left=None, image_back=None, image_right=None, model_version=None, orientation=None, texture=None, pbr=None, model_seed=None, texture_seed=None, texture_quality=None, texture_alignment=None, face_limit=None, quad=None, **kwargs): |
|
if image is None: |
|
raise RuntimeError("front image for multiview is required") |
|
images = [] |
|
image_dict = { |
|
"image": image, |
|
"image_left": image_left, |
|
"image_back": image_back, |
|
"image_right": image_right |
|
} |
|
if image_left is None and image_back is None and image_right is None: |
|
raise RuntimeError("At least one of left, back, or right image must be provided for multiview") |
|
for image_name in ["image", "image_left", "image_back", "image_right"]: |
|
image_ = image_dict[image_name] |
|
if image_ is not None: |
|
tripo_file = await upload_image_to_tripo(image_, **kwargs) |
|
images.append(tripo_file) |
|
else: |
|
images.append(TripoFileEmptyReference()) |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoMultiviewToModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoMultiviewToModelRequest( |
|
type=TripoTaskType.MULTIVIEW_TO_MODEL, |
|
files=images, |
|
model_version=model_version, |
|
orientation=orientation, |
|
texture=texture, |
|
pbr=pbr, |
|
model_seed=model_seed, |
|
texture_seed=texture_seed, |
|
texture_quality=texture_quality, |
|
texture_alignment=texture_alignment, |
|
face_limit=face_limit, |
|
quad=quad, |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoTextureNode: |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"model_task_id": ("MODEL_TASK_ID",), |
|
}, |
|
"optional": { |
|
"texture": ("BOOLEAN", {"default": True}), |
|
"pbr": ("BOOLEAN", {"default": True}), |
|
"texture_seed": ("INT", {"default": 42}), |
|
"texture_quality": (["standard", "detailed"], {"default": "standard"}), |
|
"texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) |
|
RETURN_NAMES = ("model_file", "model task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
AVERAGE_DURATION = 80 |
|
|
|
async def generate_mesh(self, model_task_id, texture=None, pbr=None, texture_seed=None, texture_quality=None, texture_alignment=None, **kwargs): |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoTextureModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoTextureModelRequest( |
|
original_model_task_id=model_task_id, |
|
texture=texture, |
|
pbr=pbr, |
|
texture_seed=texture_seed, |
|
texture_quality=texture_quality, |
|
texture_alignment=texture_alignment |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoRefineNode: |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"model_task_id": ("MODEL_TASK_ID", { |
|
"tooltip": "Must be a v1.4 Tripo model" |
|
}), |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
DESCRIPTION = "Refine a draft model created by v1.4 Tripo models only." |
|
|
|
RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) |
|
RETURN_NAMES = ("model_file", "model task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
AVERAGE_DURATION = 240 |
|
|
|
async def generate_mesh(self, model_task_id, **kwargs): |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoRefineModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoRefineModelRequest( |
|
draft_model_task_id=model_task_id |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoRigNode: |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"original_model_task_id": ("MODEL_TASK_ID",), |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "RIG_TASK_ID") |
|
RETURN_NAMES = ("model_file", "rig task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
AVERAGE_DURATION = 180 |
|
|
|
async def generate_mesh(self, original_model_task_id, **kwargs): |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoAnimateRigRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoAnimateRigRequest( |
|
original_model_task_id=original_model_task_id, |
|
out_format="glb", |
|
spec="tripo" |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoRetargetNode: |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"original_model_task_id": ("RIG_TASK_ID",), |
|
"animation": ([ |
|
"preset:idle", |
|
"preset:walk", |
|
"preset:climb", |
|
"preset:jump", |
|
"preset:slash", |
|
"preset:shoot", |
|
"preset:hurt", |
|
"preset:fall", |
|
"preset:turn", |
|
],), |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "RETARGET_TASK_ID") |
|
RETURN_NAMES = ("model_file", "retarget task_id") |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
AVERAGE_DURATION = 30 |
|
|
|
async def generate_mesh(self, animation, original_model_task_id, **kwargs): |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoAnimateRetargetRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoAnimateRetargetRequest( |
|
original_model_task_id=original_model_task_id, |
|
animation=animation, |
|
out_format="glb", |
|
bake_animation=True |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
class TripoConversionNode: |
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return { |
|
"required": { |
|
"original_model_task_id": ("MODEL_TASK_ID,RIG_TASK_ID,RETARGET_TASK_ID",), |
|
"format": (["GLTF", "USDZ", "FBX", "OBJ", "STL", "3MF"],), |
|
}, |
|
"optional": { |
|
"quad": ("BOOLEAN", {"default": False}), |
|
"face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), |
|
"texture_size": ("INT", {"min": 128, "max": 4096, "default": 4096}), |
|
"texture_format": (["BMP", "DPX", "HDR", "JPEG", "OPEN_EXR", "PNG", "TARGA", "TIFF", "WEBP"], {"default": "JPEG"}) |
|
}, |
|
"hidden": { |
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
"unique_id": "UNIQUE_ID", |
|
}, |
|
} |
|
|
|
@classmethod |
|
def VALIDATE_INPUTS(cls, input_types): |
|
|
|
|
|
if input_types["original_model_task_id"] not in ("MODEL_TASK_ID", "RIG_TASK_ID", "RETARGET_TASK_ID"): |
|
return "original_model_task_id must be MODEL_TASK_ID, RIG_TASK_ID or RETARGET_TASK_ID type" |
|
return True |
|
|
|
RETURN_TYPES = () |
|
FUNCTION = "generate_mesh" |
|
CATEGORY = "api node/3d/Tripo" |
|
API_NODE = True |
|
OUTPUT_NODE = True |
|
AVERAGE_DURATION = 30 |
|
|
|
async def generate_mesh(self, original_model_task_id, format, quad, face_limit, texture_size, texture_format, **kwargs): |
|
if not original_model_task_id: |
|
raise RuntimeError("original_model_task_id is required") |
|
response = await SynchronousOperation( |
|
endpoint=ApiEndpoint( |
|
path="/proxy/tripo/v2/openapi/task", |
|
method=HttpMethod.POST, |
|
request_model=TripoConvertModelRequest, |
|
response_model=TripoTaskResponse, |
|
), |
|
request=TripoConvertModelRequest( |
|
original_model_task_id=original_model_task_id, |
|
format=format, |
|
quad=quad if quad else None, |
|
face_limit=face_limit if face_limit != -1 else None, |
|
texture_size=texture_size if texture_size != 4096 else None, |
|
texture_format=texture_format if texture_format != "JPEG" else None |
|
), |
|
auth_kwargs=kwargs, |
|
).execute() |
|
return await poll_until_finished(kwargs, response) |
|
|
|
|
|
NODE_CLASS_MAPPINGS = { |
|
"TripoTextToModelNode": TripoTextToModelNode, |
|
"TripoImageToModelNode": TripoImageToModelNode, |
|
"TripoMultiviewToModelNode": TripoMultiviewToModelNode, |
|
"TripoTextureNode": TripoTextureNode, |
|
"TripoRefineNode": TripoRefineNode, |
|
"TripoRigNode": TripoRigNode, |
|
"TripoRetargetNode": TripoRetargetNode, |
|
"TripoConversionNode": TripoConversionNode, |
|
} |
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = { |
|
"TripoTextToModelNode": "Tripo: Text to Model", |
|
"TripoImageToModelNode": "Tripo: Image to Model", |
|
"TripoMultiviewToModelNode": "Tripo: Multiview to Model", |
|
"TripoTextureNode": "Tripo: Texture model", |
|
"TripoRefineNode": "Tripo: Refine Draft model", |
|
"TripoRigNode": "Tripo: Rig model", |
|
"TripoRetargetNode": "Tripo: Retarget rigged model", |
|
"TripoConversionNode": "Tripo: Convert model", |
|
} |
|
|