-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclassifier_agent.py
More file actions
265 lines (226 loc) · 12 KB
/
Copy pathclassifier_agent.py
File metadata and controls
265 lines (226 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
from enum import Enum
import io
import os
import threading
import time
import requests
import json
import subprocess
import logging
import traceback
import urllib.parse
import argparse
from servicestack import ResponseStatus
from PIL import Image
from classifier import load_image_models, classify_image
from audio_classifier import convert_to_wav_data, get_audio_tags_from_wav, load_audio_model
from imagehash import phash, dominant_color_hex
from utils import _log_error, create_client, config_str, _log, device_id, headers_json, load_config, to_error_status, paths, Paths
# from server import PromptServer
# from folder_paths import models_dir
# models_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../models")
from dtos import (
AgentData,
AssetType,
GetArtifactClassificationTasks,
CompleteArtifactClassificationTask,
Ratings,
)
class AudioBehavior(int, Enum):
INLINE = 0
HTTP = 1
UV = 2
# INLINE runs mediapipe in-process, but mediapipe 0.10.21 is built for numpy 1.x and
# crashes (segfault / "malloc(): unaligned tcache chunk detected") against ComfyUI's
# numpy 2.x. UV runs audio_classifier.py in its own numpy<2 env via `uv run`.
AUDIO_BEHAVIOR = AudioBehavior.UV
g_client = None
g_models = None
g_audio_model = None
g_categories = []
g_running = False
def is_enabled():
global g_config
return True #should have different options to enable classifier vs comfy-agent
# return 'enabled' in g_config and g_config['enabled'] or False
def listen_to_messages_poll():
global g_client, g_running
g_running = True
g_client = create_client()
retry_secs = 5
time.sleep(1)
global g_models
if g_models is None:
try:
g_models = load_image_models(models_dir=paths().models, debug=True)
except Exception as ex:
_log(f"Error loading image models: {ex}")
g_running = False
return
while is_enabled():
try:
g_running = True
_log("Polling for classification tasks")
request = GetArtifactClassificationTasks(device_id=device_id(),
types=[AssetType.IMAGE, AssetType.AUDIO])
response = g_client.get(request)
retry_secs = 5
artifact_ids = [f"{task.id}" for task in response.results]
if response.results is not None and len(response.results) > 0:
_log(f"Classifying {len(response.results)} artifacts: {','.join(artifact_ids)}")
for task in response.results:
update = CompleteArtifactClassificationTask(artifact_id=task.id, device_id=device_id())
type = task.type.value
try:
_log(f"Classifying {type} {task.url}...")
# printdump(task)
# download image from url and store in tmp output folder
url = urllib.parse.urljoin(config_str('url'), task.url)
_log(f"Downloading {url}")
if task.type == AssetType.IMAGE:
response = requests.get(url, headers=headers_json())
response.raise_for_status()
with Image.open(io.BytesIO(response.content)) as img:
update.phash = f"{phash(img)}"
update.color = dominant_color_hex(img)
metadata = classify_image(g_models, g_categories, img, debug=True)
update.categories = metadata["categories"]
update.tags = metadata["tags"]
update.objects = metadata["objects"]
ratings = metadata["ratings"]
update.ratings = Ratings(
predicted_rating=ratings["predicted_rating"],
confidence=ratings["confidence"],
all_scores=ratings["all_scores"])
_log(f"Classified {type} Artifact {task.id} with {len(update.tags)} tags, {len(update.objects)} objects, {len(update.categories)} categories")
elif task.type == AssetType.AUDIO:
if AUDIO_BEHAVIOR == AudioBehavior.HTTP:
start_time = time.time()
response = requests.get('http://localhost:5005/audio/tags', params={"url":url}, headers=headers_json())
response.raise_for_status()
update.tags = response.json()
elif AUDIO_BEHAVIOR == AudioBehavior.UV:
# Run in a separate process because mediapipe 0.10.21 needs numpy<2,
# which is incompatible with ComfyUI's numpy 2.x. audio_classifier.py
# carries PEP 723 inline metadata so `uv run` builds that isolated env.
try:
start_time = time.time()
script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "audio_classifier.py")
result = subprocess.run(
['uv', 'run', '--no-project', script_path, url],
capture_output=True, text=True, check=True)
tags = json.loads(result.stdout)
update.tags = tags
_log(f"Classified {type} Artifact {task.id} with {len(update.tags)} tags in {time.time() - start_time:.2f}s")
except subprocess.CalledProcessError as e:
_log_error(f"Error getting audio tags: {task.url}\nstderr: {e.stderr}", e)
update.error = to_error_status(e, message="Error getting audio tags:")
except Exception as ex:
_log_error(f"Error getting audio tags: {task.url}", ex)
update.error = to_error_status(ex, message="Error getting audio tags:")
else:
response = requests.get(url, headers=headers_json())
response.raise_for_status()
# lasy load Audio Model
global g_audio_model
if g_audio_model is None:
try:
start_time = time.time()
g_audio_model = load_audio_model(models_dir=paths().models)
_log(f"Loaded audio model ({'OK' if g_audio_model is not None else 'FAIL'}) in {time.time() - start_time:.2f}s")
except Exception as ex:
_log(f"Error loading audio model: {ex}")
if g_audio_model is not None:
start_time = time.time()
file_ext = os.path.splitext(task.url)[1].lower()
format = file_ext[1:]
try:
_log(f"Converting audio to wav: {task.url}")
sample_rate, wav_data = convert_to_wav_data(io.BytesIO(response.content), format=format)
except Exception as ex:
_log_error(f"Error converting audio to wav: {task.url}", ex)
logging.error(traceback.format_exc())
update.error = to_error_status(ex, message="Error converting audio to wav:")
continue
try:
_log(f"Getting audio tags: {task.url}, wav_data: {len(wav_data)} bytes, sample_rate: {sample_rate}")
tags = get_audio_tags_from_wav(g_audio_model, sample_rate, wav_data, debug=True) # noqa: F821
update.tags = tags
_log(f"Classified {type} Artifact {task.id} with {len(update.tags)} tags in {time.time() - start_time:.2f}s")
except Exception as ex:
_log_error(f"Error getting audio tags: {task.url}", ex)
logging.error(traceback.format_exc())
update.error = to_error_status(ex, message="Error getting audio tags:")
else:
_log("No audio model loaded")
else:
update.error = ResponseStatus(errorCode="NotImplemented", message=f"Unsupported artifact type: {task.type}")
except Exception as ex:
_log_error(f"Error classifying {task.id} {task.url}:", ex)
logging.error(traceback.format_exc())
update.error = to_error_status(ex)
finally:
# printdump(update)
try:
g_client.post(update)
except Exception as ex:
_log_error(f"Error updating Artifact {task.id}:", ex)
else:
# sleep 2s
time.sleep(2)
except Exception as ex:
_log(f"Error connecting to {config_str('url')}: {ex}, retrying in {retry_secs}s")
logging.error(traceback.format_exc())
time.sleep(retry_secs) # Wait before retrying
retry_secs += 5 # Exponential backoff
g_client = create_client() # Create new client to force reconnect
def setup(use_paths=None):
global g_client, g_running, g_installed_pip_packages, g_installed_custom_nodes, g_installed_models
if g_running:
_log("Already running")
return
load_config(agent="classifier-agent", use_paths=use_paths)
if not device_id():
return
if not config_str("url"):
_log("No URL configured. Please configure in the ComfyAgentNode.")
return
if not config_str("apikey"):
_log("No API key configured. Please configure in the ComfyAgentNode.")
return
if not is_enabled():
_log("Autostart is disabled. Enable in the ComfyAgentNode.")
return
g_running = True
g_client = create_client()
try:
global g_categories
response = g_client.get(AgentData(device_id=device_id()))
g_categories = response.categories
_log(f"Categories: {g_categories}")
return True
except Exception:
logging.error("[ERROR] Could not connect to ComfyGateway.")
logging.error(traceback.format_exc())
return False
def start(use_paths=None):
if setup(use_paths):
_log("Setting up global polling task")
t = threading.Thread(target=listen_to_messages_poll, daemon=True)
t.start()
return t
if __name__ == "__main__":
base_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../")
models_dir = os.path.join(base_path, "models")
user_dir = os.path.join(base_path, "user")
parser = argparse.ArgumentParser(description='Classifier Agent')
parser.add_argument('base_dir', help='Path to the ComfyUI base directory')
parser.add_argument('--models_dir', default=models_dir, help='Path to the ComfyUI models directory')
parser.add_argument('--user_dir', default=user_dir, help='Path to the ComfyUI user directory')
args = parser.parse_args()
setup(use_paths=Paths(
base=args.base_dir,
models=args.models_dir,
user=args.user_dir
))
listen_to_messages_poll()