-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtag_folder_ai.py
227 lines (173 loc) · 8.05 KB
/
tag_folder_ai.py
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
import json
import os
import random
from typing import Any
import anchorpoint as ap
import apsync as aps
import requests
from ai.api import init_openai_key, OPENAI_API_URL
from ai.constants import input_token_price, output_token_price
from ai.response_schema import get_folder_properties, get_folder_response_format
from ai.tokens import count_tokens
from ap_tools.dialogs import CreateTagFoldersDialogData, create_tag_folders_dialog
from common.logging import log, log_err
from common.settings import tagger_settings
from labels.attributes import ensure_attribute, replace_tag, attribute_colors
from labels.variants import engines_variants, types_variants, genres_variants
prompt = "Write tags for the folder:"
if tagger_settings.folder_use_ai_engines:
prompt += "required game engines (e.g. UE if it has *.uasset or Unity if it has *.unitypackage) or 'Any' if assets have common types, "
if tagger_settings.folder_use_ai_types:
prompt += "content types (Texture, Sprite, Model, VFX, SFX, etc.) (min 1), "
if tagger_settings.folder_use_ai_genres:
prompt += "detailed genres (min 1), "
prompt += "fill all tags"
output_token_count = 200
proceed_dialog: ap.Dialog
all_variants = {
"AI-Engines": engines_variants,
"AI-Types": types_variants,
"AI-Genres": genres_variants,
}
items = get_folder_properties()
response_format = get_folder_response_format(items)
def get_folder_structure(input_path) -> dict[Any, list[Any]]:
folder_structure = {}
for root, dirs, files in os.walk(input_path):
folder_structure[root] = files
return folder_structure
def tag_folders(workspace_id: str, input_paths: list[str], database: aps.Api, attributes: list[aps.Attribute]):
folders = []
progress = ap.Progress("Counting tokens", "Processing", infinite=False, show_loading_screen=True)
total_steps = 3
for i, input_path in enumerate(input_paths):
if os.path.isdir(input_path):
folder_structure = get_folder_structure(input_path)
progress.report_progress(i / len(input_paths) + (1 / total_steps / len(input_paths)))
folder_structure_str = str(folder_structure)
folder_name = os.path.basename(input_path)
# replace input_path with "root"
folder_structure_str = folder_structure_str.replace(input_path, "root")
progress.report_progress(i / len(input_paths) + (2 / total_steps / len(input_paths)))
log(folder_structure_str)
full_prompt = f"{prompt}\nFolder name: {folder_name}\nFolder structure:\n{folder_structure_str}"
log(full_prompt)
token_count = count_tokens(full_prompt)
progress.report_progress(i / len(input_paths) + (3 / total_steps / len(input_paths)))
input_price = token_count * input_token_price
folders.append((input_path, full_prompt, token_count, input_price))
progress.finish()
global proceed_dialog
data = CreateTagFoldersDialogData(folders, output_token_count, output_token_price)
proceed_dialog = create_tag_folders_dialog(
data,
lambda d: proceed_callback(folders, workspace_id, database, attributes))
proceed_dialog.show()
def proceed_callback(
folders: list[tuple[str, str, int, float]], workspace_id: str, database: aps.Api,
attributes: list[aps.Attribute]):
proceed_dialog.close()
def run():
progress = ap.Progress("Requesting AI tags", "Processing", infinite=False, show_loading_screen=True)
progress.report_progress(0)
for i, folder in enumerate(folders):
tag_folder(folder[1], folder[0], workspace_id, database, attributes)
progress.report_progress((i + 1) / len(folders))
progress.finish()
ctx = ap.get_context()
ctx.run_async(run)
OPENAI_API_KEY = init_openai_key()
def get_openai_response(in_prompt, model="gpt-4o-mini") -> dict:
headers = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [
{"role": "system", "content": "You are a folder tagging AI."},
{"role": "user", "content": in_prompt}
],
"response_format": response_format
}
log(f"Body: {payload}")
try:
response = requests.post(OPENAI_API_URL, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
prompt_tokens = result["usage"]["prompt_tokens"]
completion_tokens = result["usage"]["completion_tokens"]
log(f"Prompt tokens: {prompt_tokens}, Completion tokens: {completion_tokens}")
result_content = result["choices"][0]["message"]["content"].strip()
parsed = json.loads(result_content)
return parsed["items"]
except requests.exceptions.RequestException as e:
return {"error": str(e)}
except KeyError:
return {"error": "Wrong response from OpenAI"}
def tag_folder(
full_prompt: str, input_path: str, workspace_id: str, database: aps.Api,
attributes: list[aps.Attribute]):
response = get_openai_response(full_prompt)
log(response)
if response.get("error"):
err = f"Error while tagging folder: {response['error']}"
ap.UI().show_error("Error", err)
log_err(err)
return
tags = [
response["engines"] if tagger_settings.folder_use_ai_engines else None,
(response["types"] + response["types_additional"]) if tagger_settings.folder_use_ai_types else None,
(response["genres"] + response["genres_additional"]) if tagger_settings.folder_use_ai_genres else None
]
if len(tags) != len(attributes):
err = f"The number of categories ({len(tags)}) does not match the number of attributes ({len(attributes)})"
ap.UI().show_error("Error", err)
log_err(err)
return
for i, tag in enumerate(tags):
if not tag:
continue
attribute = attributes[i]
anchorpoint_tags = attribute.tags
colors = attribute_colors
# Create a set of anchorpoint tag names for faster lookup
anchorpoint_tag_names = {tag.name for tag in anchorpoint_tags}
# Add new tags from image_tags that are not already in anchorpoint_tag_names
folder_tags = tag
replaced_tags = []
for folder_tag in folder_tags:
tag = replace_tag(folder_tag.strip(), all_variants[attribute.name])
if not tag in replaced_tags:
replaced_tags.append(tag)
for folder_tag in replaced_tags:
folder_tag = folder_tag
if folder_tag not in anchorpoint_tag_names:
new_tag = aps.AttributeTag(folder_tag, random.choice(colors))
anchorpoint_tags.append(new_tag)
# Update the attribute tags in the database
database.attributes.set_attribute_tags(attribute, anchorpoint_tags)
ao_tags = aps.AttributeTagList()
for anchorpoint_tag in anchorpoint_tags:
if anchorpoint_tag.name in replaced_tags:
ao_tags.append(anchorpoint_tag)
# Set the attribute value for the input path
database.attributes.set_attribute_value(input_path, attribute, ao_tags)
def main():
if not tagger_settings.any_folder_tags_selected():
ap.UI().show_error("No tags selected", "Please select at least one tag category in the settings")
return
ctx = ap.get_context()
database = ap.get_api()
engines_attribute = ensure_attribute(database, "AI-Engines") if tagger_settings.folder_use_ai_engines else None
types_attribute = ensure_attribute(database, "AI-Types") if tagger_settings.folder_use_ai_types else None
genres_attribute = ensure_attribute(database, "AI-Genres") if tagger_settings.folder_use_ai_genres else None
attributes = [engines_attribute, types_attribute, genres_attribute]
selected_folders = ctx.selected_folders
if len(selected_folders) == 0:
selected_folders = [ctx.path]
ctx.run_async(
tag_folders, ctx.workspace_id,
selected_folders, database, attributes)
if __name__ == "__main__":
main()