fal 1.2.2__tar.gz → 1.2.4__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.
Potentially problematic release.
This version of fal might be problematic. Click here for more details.
- {fal-1.2.2 → fal-1.2.4}/PKG-INFO +1 -1
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/PKG-INFO +1 -1
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/SOURCES.txt +6 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/_fal_version.py +2 -2
- {fal-1.2.2 → fal-1.2.4}/src/fal/app.py +41 -7
- fal-1.2.4/src/fal/toolkit/image/__init__.py +74 -0
- fal-1.2.4/src/fal/toolkit/image/nsfw_filter/__init__.py +11 -0
- fal-1.2.4/src/fal/toolkit/image/nsfw_filter/env.py +9 -0
- fal-1.2.4/src/fal/toolkit/image/nsfw_filter/inference.py +77 -0
- fal-1.2.4/src/fal/toolkit/image/nsfw_filter/model.py +18 -0
- fal-1.2.4/src/fal/toolkit/image/nsfw_filter/requirements.txt +4 -0
- fal-1.2.4/src/fal/toolkit/image/safety_checker.py +107 -0
- fal-1.2.2/src/fal/toolkit/image/__init__.py +0 -3
- {fal-1.2.2 → fal-1.2.4}/.gitignore +0 -0
- {fal-1.2.2 → fal-1.2.4}/README.md +0 -0
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/dependency_links.txt +0 -0
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/entry_points.txt +0 -0
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/requires.txt +0 -0
- {fal-1.2.2 → fal-1.2.4}/fal.egg-info/top_level.txt +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/README.md +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/applications/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/applications/app_metadata.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/billing/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/billing/get_user_details.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/create_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/delete_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/get_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/list_user_workflows.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/comfy/update_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/files/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/files/check_dir_hash.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/files/upload_local_file.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/users/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/users/get_current_user.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/create_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/delete_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/get_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/update_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/client.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/errors.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/app_metadata_response_app_metadata.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/body_upload_local_file.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_detail.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_item.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs_dev_info.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/current_user.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/customer_details.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/hash_check.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/http_validation_error.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/lock_reason.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/page_workflow_item.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/team_role.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/typed_workflow_update.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/user_member.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/validation_error.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_nodes.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_item.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_node_type.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_input.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_schema_output.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/py.typed +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/types.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/pyproject.toml +0 -0
- {fal-1.2.2 → fal-1.2.4}/openapi_rest.config.yaml +0 -0
- {fal-1.2.2 → fal-1.2.4}/pyproject.toml +0 -0
- {fal-1.2.2 → fal-1.2.4}/setup.cfg +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/__main__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/_serialization.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/_version.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/api.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/apps.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/auth/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/auth/auth0.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/auth/local.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/apps.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/auth.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/create.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/debug.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/deploy.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/doctor.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/keys.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/main.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/parser.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/run.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/cli/secrets.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/console/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/console/icons.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/console/ux.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/container.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/exceptions/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/exceptions/_base.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/exceptions/auth.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/flags.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/logging/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/logging/isolate.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/logging/style.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/logging/trace.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/logging/user.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/py.typed +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/rest_client.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/sdk.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/sync.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/exceptions.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/file.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/providers/fal.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/providers/gcp.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/providers/r2.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/file/types.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/image/image.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/optimize.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/utils/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/toolkit/utils/download_utils.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/utils.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/src/fal/workflows.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_apps.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_auth.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_deploy.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_keys.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_run.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/cli/test_secrets.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/conftest.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/integration_test.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/mainify_package/__init__.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/mainify_package/impl.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/mainify_package/utils.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/mainify_target.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/test_apps.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/test_stability.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/toolkit/file_test.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tests/toolkit/image_test.py +0 -0
- {fal-1.2.2 → fal-1.2.4}/tools/demo_script.py +0 -0
{fal-1.2.2 → fal-1.2.4}/PKG-INFO
RENAMED
|
@@ -126,6 +126,12 @@ src/fal/toolkit/file/providers/gcp.py
|
|
|
126
126
|
src/fal/toolkit/file/providers/r2.py
|
|
127
127
|
src/fal/toolkit/image/__init__.py
|
|
128
128
|
src/fal/toolkit/image/image.py
|
|
129
|
+
src/fal/toolkit/image/safety_checker.py
|
|
130
|
+
src/fal/toolkit/image/nsfw_filter/__init__.py
|
|
131
|
+
src/fal/toolkit/image/nsfw_filter/env.py
|
|
132
|
+
src/fal/toolkit/image/nsfw_filter/inference.py
|
|
133
|
+
src/fal/toolkit/image/nsfw_filter/model.py
|
|
134
|
+
src/fal/toolkit/image/nsfw_filter/requirements.txt
|
|
129
135
|
src/fal/toolkit/utils/__init__.py
|
|
130
136
|
src/fal/toolkit/utils/download_utils.py
|
|
131
137
|
tests/__init__.py
|
|
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import queue
|
|
6
7
|
import re
|
|
8
|
+
import threading
|
|
7
9
|
import time
|
|
8
10
|
import typing
|
|
9
11
|
from contextlib import asynccontextmanager, contextmanager
|
|
@@ -72,17 +74,22 @@ def wrap_app(cls: type[App], **kwargs) -> fal.api.IsolatedFunction:
|
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
class EndpointClient:
|
|
75
|
-
def __init__(self, url, endpoint, signature):
|
|
77
|
+
def __init__(self, url, endpoint, signature, timeout: int | None = None):
|
|
76
78
|
self.url = url
|
|
77
79
|
self.endpoint = endpoint
|
|
78
80
|
self.signature = signature
|
|
81
|
+
self.timeout = timeout
|
|
79
82
|
|
|
80
83
|
annotations = endpoint.__annotations__ or {}
|
|
81
84
|
self.return_type = annotations.get("return") or None
|
|
82
85
|
|
|
83
86
|
def __call__(self, data):
|
|
84
87
|
with httpx.Client() as client:
|
|
85
|
-
resp = client.post(
|
|
88
|
+
resp = client.post(
|
|
89
|
+
self.url + self.signature.path,
|
|
90
|
+
json=data.dict() if hasattr(data, "dict") else dict(data),
|
|
91
|
+
timeout=self.timeout,
|
|
92
|
+
)
|
|
86
93
|
resp.raise_for_status()
|
|
87
94
|
resp_dict = resp.json()
|
|
88
95
|
|
|
@@ -93,7 +100,12 @@ class EndpointClient:
|
|
|
93
100
|
|
|
94
101
|
|
|
95
102
|
class AppClient:
|
|
96
|
-
def __init__(
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
cls,
|
|
106
|
+
url,
|
|
107
|
+
timeout: int | None = None,
|
|
108
|
+
):
|
|
97
109
|
self.url = url
|
|
98
110
|
self.cls = cls
|
|
99
111
|
|
|
@@ -101,19 +113,38 @@ class AppClient:
|
|
|
101
113
|
signature = getattr(endpoint, "route_signature", None)
|
|
102
114
|
if signature is None:
|
|
103
115
|
continue
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
endpoint_client = EndpointClient(
|
|
117
|
+
self.url,
|
|
118
|
+
endpoint,
|
|
119
|
+
signature,
|
|
120
|
+
timeout=timeout,
|
|
121
|
+
)
|
|
122
|
+
setattr(self, name, endpoint_client)
|
|
106
123
|
|
|
107
124
|
@classmethod
|
|
108
125
|
@contextmanager
|
|
109
126
|
def connect(cls, app_cls):
|
|
110
127
|
app = wrap_app(app_cls)
|
|
111
128
|
info = app.spawn()
|
|
129
|
+
_shutdown_event = threading.Event()
|
|
130
|
+
|
|
131
|
+
def _print_logs():
|
|
132
|
+
while not _shutdown_event.is_set():
|
|
133
|
+
try:
|
|
134
|
+
log = info.logs.get(timeout=0.1)
|
|
135
|
+
except queue.Empty:
|
|
136
|
+
continue
|
|
137
|
+
print(log)
|
|
138
|
+
|
|
139
|
+
_log_printer = threading.Thread(target=_print_logs, daemon=True)
|
|
140
|
+
_log_printer.start()
|
|
141
|
+
|
|
112
142
|
try:
|
|
113
143
|
with httpx.Client() as client:
|
|
114
144
|
retries = 100
|
|
115
145
|
while retries:
|
|
116
146
|
resp = client.get(info.url + "/health")
|
|
147
|
+
|
|
117
148
|
if resp.is_success:
|
|
118
149
|
break
|
|
119
150
|
elif resp.status_code != 500:
|
|
@@ -121,9 +152,12 @@ class AppClient:
|
|
|
121
152
|
time.sleep(0.1)
|
|
122
153
|
retries -= 1
|
|
123
154
|
|
|
124
|
-
|
|
155
|
+
client = cls(app_cls, info.url)
|
|
156
|
+
yield client
|
|
125
157
|
finally:
|
|
126
158
|
info.stream.cancel()
|
|
159
|
+
_shutdown_event.set()
|
|
160
|
+
_log_printer.join()
|
|
127
161
|
|
|
128
162
|
def health(self):
|
|
129
163
|
with httpx.Client() as client:
|
|
@@ -158,7 +192,7 @@ class App(fal.api.BaseServable):
|
|
|
158
192
|
app_name = kwargs.pop("name", None) or _to_fal_app_name(cls.__name__)
|
|
159
193
|
parent_settings = getattr(cls, "host_kwargs", {})
|
|
160
194
|
cls.host_kwargs = {**parent_settings, **kwargs}
|
|
161
|
-
cls.app_name = app_name
|
|
195
|
+
cls.app_name = getattr(cls, "app_name", app_name)
|
|
162
196
|
|
|
163
197
|
if cls.__init__ is not App.__init__:
|
|
164
198
|
raise ValueError(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
|
|
7
|
+
from .image import * # noqa: F403
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from PIL.Image import Image as PILImage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def filter_by(
|
|
14
|
+
has_nsfw_concepts: list[bool],
|
|
15
|
+
images: list[PILImage],
|
|
16
|
+
) -> list[PILImage]:
|
|
17
|
+
from PIL import Image as PILImage
|
|
18
|
+
|
|
19
|
+
return [
|
|
20
|
+
(
|
|
21
|
+
PILImage.new("RGB", (image.width, image.height), (0, 0, 0))
|
|
22
|
+
if has_nsfw
|
|
23
|
+
else image
|
|
24
|
+
)
|
|
25
|
+
for image, has_nsfw in zip(images, has_nsfw_concepts)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def preprocess_image(image_pil, convert_to_rgb=True, fix_orientation=True):
|
|
30
|
+
from PIL import ImageOps, ImageSequence
|
|
31
|
+
|
|
32
|
+
# For MPO (multi picture object) format images, we only need the first image
|
|
33
|
+
images = []
|
|
34
|
+
for image in ImageSequence.Iterator(image_pil):
|
|
35
|
+
img = image
|
|
36
|
+
|
|
37
|
+
if convert_to_rgb:
|
|
38
|
+
img = img.convert("RGB")
|
|
39
|
+
|
|
40
|
+
if fix_orientation:
|
|
41
|
+
img = ImageOps.exif_transpose(img)
|
|
42
|
+
|
|
43
|
+
images.append(img)
|
|
44
|
+
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
return images[0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@lru_cache(maxsize=64)
|
|
51
|
+
def read_image_from_url(
|
|
52
|
+
url: str, convert_to_rgb: bool = True, fix_orientation: bool = True
|
|
53
|
+
):
|
|
54
|
+
from fastapi import HTTPException
|
|
55
|
+
from PIL import Image
|
|
56
|
+
|
|
57
|
+
TEMP_HEADERS = {
|
|
58
|
+
"User-Agent": (
|
|
59
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) "
|
|
60
|
+
"Gecko/20100101 Firefox/21.0"
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
request = Request(url, headers=TEMP_HEADERS)
|
|
66
|
+
response = urlopen(request)
|
|
67
|
+
image_pil = Image.open(response)
|
|
68
|
+
except Exception:
|
|
69
|
+
import traceback
|
|
70
|
+
|
|
71
|
+
traceback.print_exc()
|
|
72
|
+
raise HTTPException(422, f"Could not load image from url: {url}")
|
|
73
|
+
|
|
74
|
+
return preprocess_image(image_pil, convert_to_rgb, fix_orientation)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
import fal
|
|
4
|
+
from fal.toolkit.image import read_image_from_url
|
|
5
|
+
|
|
6
|
+
from .env import get_requirements
|
|
7
|
+
from .model import get_model
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NSFWImageDetectionInput(BaseModel):
|
|
11
|
+
image_url: str = Field(
|
|
12
|
+
description="Input image url.",
|
|
13
|
+
examples=[
|
|
14
|
+
"https://storage.googleapis.com/falserverless/model_tests/remove_background/elephant.jpg",
|
|
15
|
+
],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NSFWImageDetectionOutput(BaseModel):
|
|
20
|
+
nsfw_probability: float = Field(
|
|
21
|
+
description="The probability of the image being NSFW.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_nsfw_content(pil_image: object):
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
model, processor = get_model()
|
|
29
|
+
|
|
30
|
+
with torch.no_grad():
|
|
31
|
+
inputs = processor(images=pil_image, return_tensors="pt")
|
|
32
|
+
outputs = model(**inputs)
|
|
33
|
+
logits = outputs.logits.squeeze() # Remove batch dimension to simplify indexing
|
|
34
|
+
|
|
35
|
+
# Apply softmax to convert logits to probabilities
|
|
36
|
+
probabilities = torch.softmax(logits, dim=0)
|
|
37
|
+
|
|
38
|
+
nsfw_class_index = model.config.label2id.get(
|
|
39
|
+
"nsfw", None
|
|
40
|
+
) # Replace "NSFW" with the exact class name if different
|
|
41
|
+
|
|
42
|
+
# Validate that NSFW class index is found
|
|
43
|
+
if nsfw_class_index is not None:
|
|
44
|
+
nsfw_probability = probabilities[int(nsfw_class_index)].item()
|
|
45
|
+
return nsfw_probability
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("NSFW class not found in model output.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_nsfw_estimation(
|
|
51
|
+
input: NSFWImageDetectionInput,
|
|
52
|
+
) -> NSFWImageDetectionOutput:
|
|
53
|
+
img = read_image_from_url(input.image_url)
|
|
54
|
+
nsfw_probability = check_nsfw_content(img)
|
|
55
|
+
|
|
56
|
+
return NSFWImageDetectionOutput(nsfw_probability=nsfw_probability)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@fal.function(
|
|
60
|
+
requirements=get_requirements(),
|
|
61
|
+
machine_type="GPU-A6000",
|
|
62
|
+
serve=True,
|
|
63
|
+
)
|
|
64
|
+
def run_nsfw_estimation_on_fal(
|
|
65
|
+
input: NSFWImageDetectionInput,
|
|
66
|
+
) -> NSFWImageDetectionOutput:
|
|
67
|
+
return run_nsfw_estimation(input)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
local = run_nsfw_estimation_on_fal.on(serve=False)
|
|
72
|
+
result = local(
|
|
73
|
+
NSFWImageDetectionInput(
|
|
74
|
+
image_url="https://storage.googleapis.com/falserverless/model_tests/remove_background/elephant.jpg",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
print(result)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@fal.cached
|
|
5
|
+
def get_model():
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from transformers import AutoModelForImageClassification, ViTImageProcessor
|
|
9
|
+
|
|
10
|
+
os.environ["TRANSFORMERS_CACHE"] = "/data/models"
|
|
11
|
+
os.environ["HF_HOME"] = "/data/models"
|
|
12
|
+
|
|
13
|
+
model = AutoModelForImageClassification.from_pretrained(
|
|
14
|
+
"Falconsai/nsfw_image_detection"
|
|
15
|
+
)
|
|
16
|
+
processor = ViTImageProcessor.from_pretrained("Falconsai/nsfw_image_detection")
|
|
17
|
+
|
|
18
|
+
return model, processor
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import fal
|
|
4
|
+
|
|
5
|
+
from . import filter_by
|
|
6
|
+
from .nsfw_filter.model import get_model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@fal.cached
|
|
10
|
+
def load_safety_checker():
|
|
11
|
+
import torch
|
|
12
|
+
from diffusers.pipelines.stable_diffusion.safety_checker import (
|
|
13
|
+
StableDiffusionSafetyChecker,
|
|
14
|
+
)
|
|
15
|
+
from transformers import AutoFeatureExtractor
|
|
16
|
+
|
|
17
|
+
feature_extractor = AutoFeatureExtractor.from_pretrained(
|
|
18
|
+
"CompVis/stable-diffusion-safety-checker",
|
|
19
|
+
torch_dtype="float16",
|
|
20
|
+
)
|
|
21
|
+
safety_checker = StableDiffusionSafetyChecker.from_pretrained(
|
|
22
|
+
"CompVis/stable-diffusion-safety-checker",
|
|
23
|
+
torch_dtype=torch.float16,
|
|
24
|
+
).to("cuda")
|
|
25
|
+
|
|
26
|
+
return feature_extractor, safety_checker
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_safety_checker(
|
|
30
|
+
pil_images: list[object],
|
|
31
|
+
) -> list[bool]:
|
|
32
|
+
import numpy as np
|
|
33
|
+
import torch
|
|
34
|
+
|
|
35
|
+
feature_extractor, safety_checker = load_safety_checker()
|
|
36
|
+
|
|
37
|
+
safety_checker_input = feature_extractor(pil_images, return_tensors="pt").to("cuda")
|
|
38
|
+
|
|
39
|
+
np_image = [np.array(val) for val in pil_images]
|
|
40
|
+
|
|
41
|
+
_, has_nsfw_concept = safety_checker(
|
|
42
|
+
images=np_image,
|
|
43
|
+
clip_input=safety_checker_input.pixel_values.to(torch.float16),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return has_nsfw_concept
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_safety_checker_v2(pil_images: list, nsfw_threshold: float = 0.5) -> list[bool]:
|
|
50
|
+
import torch
|
|
51
|
+
|
|
52
|
+
model, processor = get_model()
|
|
53
|
+
|
|
54
|
+
has_nsfw_concept = []
|
|
55
|
+
|
|
56
|
+
with torch.no_grad():
|
|
57
|
+
for pil_image in pil_images:
|
|
58
|
+
inputs = processor(
|
|
59
|
+
images=pil_image.convert("RGB"),
|
|
60
|
+
return_tensors="pt",
|
|
61
|
+
)
|
|
62
|
+
outputs = model(**inputs)
|
|
63
|
+
logits = (
|
|
64
|
+
outputs.logits.squeeze()
|
|
65
|
+
) # Remove batch dimension to simplify indexing
|
|
66
|
+
|
|
67
|
+
# Apply softmax to convert logits to probabilities
|
|
68
|
+
probabilities = torch.softmax(logits, dim=0)
|
|
69
|
+
|
|
70
|
+
nsfw_class_index = model.config.label2id.get(
|
|
71
|
+
"nsfw", None
|
|
72
|
+
) # Replace "NSFW" with the exact class name if different
|
|
73
|
+
|
|
74
|
+
# Validate that NSFW class index is found
|
|
75
|
+
if nsfw_class_index is not None:
|
|
76
|
+
nsfw_probability = probabilities[int(nsfw_class_index)].item()
|
|
77
|
+
print("NSFW probability:", nsfw_probability)
|
|
78
|
+
has_nsfw_concept.append(nsfw_probability > nsfw_threshold)
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("NSFW class not found in model output.")
|
|
81
|
+
|
|
82
|
+
return has_nsfw_concept
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def postprocess_images(
|
|
86
|
+
pil_images: list[object],
|
|
87
|
+
enable_safety_checker: bool = True,
|
|
88
|
+
safety_checker_version: int = 2,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
outputs: dict[str, list[Any]] = {
|
|
91
|
+
"images": pil_images,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if enable_safety_checker:
|
|
95
|
+
safety_checker_fn = (
|
|
96
|
+
run_safety_checker_v2 if safety_checker_version == 2 else run_safety_checker
|
|
97
|
+
)
|
|
98
|
+
outputs["has_nsfw_concepts"] = safety_checker_fn(pil_images) # type: ignore
|
|
99
|
+
else:
|
|
100
|
+
outputs["has_nsfw_concepts"] = [False] * len(pil_images)
|
|
101
|
+
|
|
102
|
+
outputs["images"] = filter_by(
|
|
103
|
+
outputs["has_nsfw_concepts"],
|
|
104
|
+
outputs["images"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return outputs
|
|
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
|
|
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
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/api/workflows/list_user_workflows.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
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_extra_data.py
RENAMED
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_fal_inputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/comfy_workflow_schema_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/page_comfy_workflow_item.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/typed_comfy_workflow_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_contents_output.py
RENAMED
|
File without changes
|
|
File without changes
|
{fal-1.2.2 → fal-1.2.4}/openapi-fal-rest/openapi_fal_rest/models/workflow_detail_contents.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
|
|
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
|
|
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
|
|
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
|
|
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
|