diff --git a/.env_example b/.env_example index 31ca32a..a2cbb86 100644 --- a/.env_example +++ b/.env_example @@ -1,15 +1,22 @@ -NOSTR_PRIVATE_KEY = "nostrSecretkeyinhex" -NOSTR_TEST_CLIENT_PRIVATE_KEY = "nostrSecretkeyinhex_forthetestclient" -USER_DB_PATH = "nostrzaps.db" +NOSTR_PRIVATE_KEY = "a secret hexkey for some demo dvms" +NOSTR_PRIVATE_KEY2 = "another secret hexkey for demo dvm with another key" +BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" +NOSTR_TEST_CLIENT_PRIVATE_KEY = "a secret hex key for the test dvm client" -# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile) + +# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile, make sure to set one) LNBITS_INVOICE_KEY = "" LNBITS_HOST = "https://lnbits.com" -TASK_TEXTEXTRACTION_NIP89_DTAG = "asdd" + +# Some d tags we use in the testfile to announce or dvms. Create one at vendata.io) +TASK_TEXT_EXTRACTION_NIP89_DTAG = "asdd" TASK_TRANSLATION_NIP89_DTAG = "abcded" -TASK_IMAGEGENERATION_NIP89_DTAG = "fgdfgdf" -TASK_IMAGEGENERATION_NIP89_DTAG2 = "fgdfgdf" +TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" +TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" #Backend Specific Options for tasks that require them + +#nova-server is a local backend supporting some AI modules and needs to be installed separately, +#if dvms supporting it should be used NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/backends/nova_server.py b/backends/nova_server.py index 9142193..885e352 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -62,7 +62,7 @@ def check_nova_server_status(jobID, address): log = log_content[length:] length = len(log_content) if log != "": - print(log + " Status: " + str(status)) + print(log) # WAITING = 0, RUNNING = 1, FINISHED = 2, ERROR = 3 time.sleep(1.0) diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..d23ead4 --- /dev/null +++ b/bot.py @@ -0,0 +1,160 @@ +import json +import time +from threading import Thread + +from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, Event + +from utils.admin_utils import admin_make_database_updates +from utils.database_utils import get_or_add_user, update_user_balance +from utils.definitions import EventDefinitions +from utils.nostr_utils import send_event, get_event_by_id +from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message + + +class Bot: + job_list: list + + def __init__(self, dvm_config, admin_config=None): + self.dvm_config = dvm_config + self.admin_config = admin_config + self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) + self.client = Client(self.keys) + self.job_list = [] + + pk = self.keys.public_key() + self.dvm_config.DB = "db/bot.db" + + print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + + for relay in self.dvm_config.RELAY_LIST: + self.client.add_relay(relay) + self.client.connect() + + dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, EventDefinitions.KIND_ZAP]).since( + Timestamp.now()) + self.client.subscribe([dm_zap_filter]) + + admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) + + class NotificationHandler(HandleNotification): + client = self.client + dvm_config = self.dvm_config + keys = self.keys + + def handle(self, relay_url, nostr_event): + if EventDefinitions.KIND_DM: + print( + "[Bot] " + f"Received new DM from {relay_url}: {nostr_event.as_json()}") + handle_dm(nostr_event) + elif nostr_event.kind() == EventDefinitions.KIND_ZAP: + print("yay zap") + handle_zap(nostr_event) + + def handle_msg(self, relay_url, msg): + return + + def handle_dm(nostr_event): + sender = nostr_event.pubkey().to_hex() + try: + decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) + # TODO more advanced logic, more parsing, just very basic test functions for now + if decrypted_text[0].isdigit(): + index = int(decrypted_text.split(' ')[0]) - 1 + i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") + + keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_TASKS[index].PK) + params = { + "sender": nostr_event.pubkey().to_hex(), + "input": i_tag, + "task": self.dvm_config.SUPPORTED_TASKS[index].TASK + } + message = json.dumps(params) + evt = EventBuilder.new_encrypted_direct_msg(self.keys, keys.public_key(), + message, None).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) + + elif decrypted_text.startswith('{"result":'): + + dvm_result = json.loads(decrypted_text) + + job_event = EventBuilder.new_encrypted_direct_msg(self.keys, + PublicKey.from_hex(dvm_result["sender"]), + dvm_result["result"], + None).to_event(self.keys) + send_event(job_event, client=self.client, dvm_config=dvm_config) + user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) + print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) + + + else: + message = "DVMs that I support:\n\n" + index = 1 + for p in self.dvm_config.SUPPORTED_TASKS: + message += str(index) + " " + p.NAME + " " + p.TASK + "\n" + index += 1 + + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + message + "\nSelect an Index and provide an input (" + "e.g. 1 A purple ostrich)", + nostr_event.id()).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) + except Exception as e: + print(e) + + def handle_zap(zap_event): + zapped_event = None + invoice_amount = 0 + anon = False + sender = zap_event.pubkey() + print("Zap received") + + try: + for tag in zap_event.tags(): + if tag.as_vec()[0] == 'bolt11': + invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) + elif tag.as_vec()[0] == 'e': + zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) + elif tag.as_vec()[0] == 'description': + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for ztag in zap_request_event.tags(): + if ztag.as_vec()[0] == 'anon': + if len(ztag.as_vec()) > 1: + print("Private Zap received.") + decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], + self.keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + if message != "": + print("Zap Message: " + message) + else: + anon = True + print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") + user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) + print(str(user.name)) + + if zapped_event is not None: + if not anon: + print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) + + # a regular note + elif not anon: + print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) + + except Exception as e: + print(f"Error during content decryption: {e}") + + self.client.handle_notifications(NotificationHandler()) + while True: + time.sleep(1.0) diff --git a/dvm.py b/dvm.py index 2fb5a64..6201646 100644 --- a/dvm.py +++ b/dvm.py @@ -1,5 +1,7 @@ +import json + from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel + init_logger, LogLevel, nip04_decrypt import time @@ -39,21 +41,23 @@ class DVM: pk = self.keys.public_key() - print("Nostr DVM public key: " + str(pk.to_bech32()) + "Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) self.client.connect() - dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(Timestamp.now()) + #TODO only from allowed account kinds = [EventDefinitions.KIND_NIP90_GENERIC] for dvm in self.dvm_config.SUPPORTED_TASKS: if dvm.KIND not in kinds: kinds.append(dvm.KIND) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) - self.client.subscribe([dm_zap_filter, dvm_filter]) + self.client.subscribe([dvm_filter, zap_filter, bot_dm_filter]) create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) @@ -69,6 +73,8 @@ class DVM: handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) + elif nostr_event.kind() == EventDefinitions.KIND_DM: + handle_dm(nostr_event) def handle_msg(self, relay_url, msg): return @@ -84,7 +90,7 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: - print("[" + self.dvm_config.NIP89.name + "] Received new Task: " + task + " from " + user.name) + print("[" + self.dvm_config.NIP89.name + "] Received new Request: " + task + " from " + user.name) amount = get_amount_per_task(task, self.dvm_config, duration) if amount is None: return @@ -218,6 +224,23 @@ class DVM: except Exception as e: print(f"Error during content decryption: {e}") + def handle_dm(dm_event): + decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content()) + ob = json.loads(decrypted_text) + + #TODO SOME PARSING, OPTIONS, ZAP HANDLING + + # One key might host multiple dvms, so we check current task + if ob['task'] == self.dvm_config.SUPPORTED_TASKS[0].TASK: + j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_TASKS[0].TASK]) + i_tag = Tag.parse(["i", ob['input'], "text"]) + tags = [j_tag, i_tag] + tags.append(Tag.parse(["y", dm_event.pubkey().to_hex()])) + tags.append(Tag.parse(["z", ob['sender']])) + job_event = EventBuilder(EventDefinitions.KIND_DM, "", tags).to_event(self.keys) + + do_work(job_event, is_from_bot=True) + def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) if not task_supported: @@ -245,7 +268,7 @@ class DVM: else: return True - def check_and_return_event(data, original_event_str: str): + def check_and_return_event(data, original_event_str: str, is_from_bot: bool): original_event = Event.from_json(original_event_str) for x in self.job_list: @@ -271,7 +294,22 @@ class DVM: try: post_processed_content = post_process_result(data, original_event) - send_nostr_reply_event(post_processed_content, original_event_str) + if is_from_bot: + for tag in original_event.tags(): + if tag.as_vec()[0] == "y": # TODO we temporally use internal tags to move information + receiver_key = PublicKey.from_hex(tag.as_vec()[1]) + elif tag.as_vec()[0] == "z": + original_sender = tag.as_vec()[1] + + params = { + "result": post_processed_content, + "sender": original_sender + } + message = json.dumps(params) + response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) + send_event(response_event, client=self.client, dvm_config=self.dvm_config) + else: + send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: respond_to_error(str(e), original_event_str, False) @@ -401,11 +439,11 @@ class DVM: request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, self.dvm_config) result = dvm.process(request_form) - check_and_return_event(result, str(job_event.as_json())) + check_and_return_event(result, str(job_event.as_json()), is_from_bot=is_from_bot) except Exception as e: print(e) - respond_to_error(str(e), job_event.as_json(), is_from_bot) + respond_to_error(str(e), job_event.as_json(), is_from_bot=is_from_bot) return self.client.handle_notifications(NotificationHandler()) @@ -447,3 +485,4 @@ class DVM: self.jobs_on_hold_list.remove(job) time.sleep(1.0) + diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 426fbcd..32efa2c 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -1,6 +1,10 @@ import json +from threading import Thread +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Announcement +from dvm import DVM class DVMTaskInterface: @@ -9,6 +13,9 @@ class DVMTaskInterface: TASK: str COST: int PK: str + DVM = DVM + dvm_config: DVMConfig + admin_config: AdminConfig def NIP89_announcement(self, d_tag, content): nip89 = NIP89Announcement() @@ -19,6 +26,10 @@ class DVMTaskInterface: nip89.content = content return nip89 + def run(self): + nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) + nostr_dvm_thread.start() + def is_input_supported(self, input_type, input_content) -> bool: """Check if input is supported for current Task.""" pass diff --git a/main.py b/main.py index dd10f61..d01efd1 100644 --- a/main.py +++ b/main.py @@ -3,98 +3,49 @@ from pathlib import Path from threading import Thread import dotenv -import utils.env as env -from tasks.imagegenerationsdxl import ImageGenerationSDXL -from tasks.textextractionpdf import TextExtractionPDF -from tasks.translation import Translation -from utils.admin_utils import AdminConfig +from nostr_sdk import Keys + +from bot import Bot +from playground import build_pdf_extractor, build_translator, build_unstable_diffusion, build_sketcher from utils.dvmconfig import DVMConfig def run_nostr_dvm_with_local_config(): - #Generate a optional Admin Config, in this case, whenever we give our DVMS this config, they will (re)broadcast - # their NIP89 announcement - admin_config = AdminConfig() - admin_config.REBROADCASTNIP89 = False + # We extract the Publickey from our bot, so the DVMs know who they should listen and react to. + bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key().to_hex() - # Spawn the DVMs - # Add NIP89 events for each DVM - # Add the dtag here or in your .env file, so you can update your dvm later and change the content as needed. - # Get a dtag and the content at vendata.io + # Spawn some DVMs in the playground and run them + # You can add arbitrary DVMs there and instantiate them here # Spawn DVM1 Kind 5000 Text Extractor from PDFs - - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - - pdfextactor = TextExtractionPDF("PDF Extractor", dvm_config) - d_tag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) - content = ("{\"name\":\"" + pdfextactor.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I extract Text from pdf documents\"," - "\"nip90Params\":{}}") - dvm_config.NIP89 = pdfextactor.NIP89_announcement(d_tag, content) + pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey]) + pdfextractor.run() # Spawn DVM2 Kind 5002 Text Translation - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + translator = build_translator("Translator", [bot_publickey]) + translator.run() - translator = Translation("Translator", dvm_config) - d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) - content = ("{\"name\":\"" + translator.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I translate text from given text/event/job, ""currently using Google Translation Services into language defined in params.\"," - "\"nip90Params\":{\"language\":{\"required\":true," - "\"values\":[\"af\",\"am\",\"ar\",\"az\",\"be\",\"bg\",\"bn\"," - "\"bs\",\"ca\",\"ceb\",\"co\",\"cs\",\"cy\",\"da\",\"de\",\"el\"," - "\"eo\",\"es\",\"et\",\"eu\",\"fa\",\"fi\",\"fr\",\"fy\",\"ga\"," - "\"gd\",\"gl\",\"gu\",\"ha\",\"haw\",\"hi\",\"hmn\",\"hr\",\"ht\"," - "\"hu\",\"hy\",\"id\",\"ig\",\"is\",\"it\",\"he\",\"ja\",\"jv\"," - "\"ka\",\"kk\",\"km\",\"kn\",\"ko\",\"ku\",\"ky\",\"la\",\"lb\"," - "\"lo\",\"lt\",\"lv\",\"mg\",\"mi\",\"mk\",\"ml\",\"mn\",\"mr\"," - "\"ms\",\"mt\",\"my\",\"ne\",\"nl\",\"no\",\"ny\",\"or\",\"pa\"," - "\"pl\",\"ps\",\"pt\",\"ro\",\"ru\",\"sd\",\"si\",\"sk\",\"sl\"," - "\"sm\",\"sn\",\"so\",\"sq\",\"sr\",\"st\",\"su\",\"sv\",\"sw\"," - "\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\"," - "\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}") + # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. + # If you want to use it, see the instructions in backends/nova_server + unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey]) + unstable_artist.run() - dvm_config.NIP89 = translator.NIP89_announcement(d_tag, content) - - # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. If you want to use - # it see the instructions in backends/nova_server - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - content = ("{\"name\":\"" + unstableartist.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," - "\"nip90Params\":{}}") - dvm_config.NIP89 = unstableartist.NIP89_announcement(d_tag, content) - - - # Spawn another Instance of text-to-image but use a different privatekey, model and lora this time. - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = "73b262d31edc6ea1316dffcc7daa772651d661e6475761b7b78291482c1bf5cb" - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - #We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 - sketcher = ImageGenerationSDXL("Sketcher", dvm_config, admin_config, default_model="mohawk", default_lora="timburton") - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) - content = ("{\"name\":\"" + sketcher.NAME + "\"," - "\"image\":\"https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg\"," - "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," - "\"nip90Params\":{}}") - - dvm_config.NIP89 = sketcher.NIP89_announcement(d_tag, content) + # Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time. + sketcher = build_sketcher("Sketcher", [bot_publickey]) + sketcher.run() + # We will run an optional bot that can communicate with the DVMs + # Note this is very basic for now and still under development + bot_config = DVMConfig() + bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") + bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") + # Finally we add some of the DVMs we created before to the Bot and start it. + bot_config.SUPPORTED_TASKS = [sketcher, unstable_artist, translator] + bot = Bot + nostr_dvm_thread = Thread(target=bot, args=[bot_config]) + nostr_dvm_thread.start() if __name__ == '__main__': diff --git a/playground.py b/playground.py new file mode 100644 index 0000000..6dfab48 --- /dev/null +++ b/playground.py @@ -0,0 +1,129 @@ +import json +import os + +from tasks.imagegenerationsdxl import ImageGenerationSDXL +from tasks.textextractionpdf import TextExtractionPDF +from tasks.translation import Translation +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig + +# Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast +# their NIP89 announcement +admin_config = AdminConfig() +admin_config.REBROADCAST_NIP89 = False + + +def build_pdf_extractor(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + # Add NIP89 + d_tag = os.getenv("TASK_TEXT_EXTRACTION_NIP89_DTAG") + nip90params = {} + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I extract text from pdf documents", + "nip90Params": nip90params + } + return TextExtractionPDF(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config) + + +def build_translator(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + d_tag = os.getenv("TASK_TRANSLATION_NIP89_DTAG") + nip90params = { + "language": { + "required": False, + "values": ["en", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co", "cs", "cy", "da", "de", "el", "eo", "es", + "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl","gu", "ha", "haw", "hi", "hmn", "hr", "ht", + "hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk","km", "kn", "ko", "ku", "ky", + "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl", + "no", "ny", "or", "pa", "pl", "ps", "pt", "ro", "ru", "sd", "si", "sk", "sl", "sm", "sn", "so", + "sq", "sr", "st", "su", "sv", "sw", "ta", "te","tg", "th", "tl", "tr", "ug", "uk", "ur", "uz", + "vi", "xh", "yi", "yo", "zh", "zu"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I translate text from given text/event/job. Currently using Google Translation Services to translate " + "input into the language defined in params.", + "nip90Params": nip90params + } + return Translation(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config) + + +def build_unstable_diffusion(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + # A module might have options it can be initialized with, here we set a default model, and the nova-server + # address it should use. These parameters can be freely defined in the task component + options = {'default_model': "unstable", 'nova_server': os.getenv("NOVA_SERVER")} + + d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") + nip90params = { + "negative_prompt": { + "required": False, + "values": [] + }, + "ratio": { + "required": False, + "values": ["1:1", "4:3", "16:9", "3:4", "9:16", "10:16"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I draw images based on a prompt with a Model called unstable diffusion", + "nip90Params": nip90params + } + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config, options=options) + + +def build_sketcher(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") + nip90params = { + "negative_prompt": { + "required": False, + "values": [] + }, + "ratio": { + "required": False, + "values": ["1:1", "4:3", "16:9", "3:4", "9:16", "10:16"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg", + "about": "I draw images based on a prompt in the style of paper sketches", + "nip90Params": nip90params + } + + # A module might have options it can be initialized with, here we set a default model, lora and the nova-server + # address it should use. These parameters can be freely defined in the task component + options = {'default_model': "mohawk", 'default_lora': "timburton", 'nova_server': os.getenv("NOVA_SERVER")} + + # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config, options=options) \ No newline at end of file diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 524c437..0af0be0 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -26,19 +26,18 @@ class ImageGenerationSDXL(DVMTaskInterface): TASK: str = "text-to-image" COST: int = 50 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, - default_lora=None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None, options=None): self.NAME = name + self.PK = dvm_config.PRIVATE_KEY + dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" - self.PK = dvm_config.PRIVATE_KEY - self.default_model = default_model - self.default_lora = default_lora - - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + self.options = options def is_input_supported(self, input_type, input_content): if input_type != "text": @@ -51,19 +50,19 @@ class ImageGenerationSDXL(DVMTaskInterface): prompt = "" negative_prompt = "" - if self.default_model is None: - model = "stabilityai/stable-diffusion-xl-base-1.0" + if self.options.get("default_model"): + model = self.options['default_model'] else: - model = self.default_model + model = "stabilityai/stable-diffusion-xl-base-1.0" ratio_width = "1" ratio_height = "1" width = "" height = "" - if self.default_lora == None: - lora = "" + if self.options.get("default_lora"): + lora = self.options['default_lora'] else: - lora = self.default_lora + lora = "" lora_weight = "" strength = "" guidance_scale = "" @@ -149,12 +148,12 @@ class ImageGenerationSDXL(DVMTaskInterface): def process(self, request_form): try: # Call the process route of NOVA-Server with our request form. - response = send_request_to_nova_server(request_form, os.environ["NOVA_SERVER"]) + response = send_request_to_nova_server(request_form, self.options['nova_server']) if bool(json.loads(response)['success']): print("Job " + request_form['jobID'] + " sent to nova-server") pool = ThreadPool(processes=1) - thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], os.environ["NOVA_SERVER"])) + thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], self.options['nova_server'])) print("Wait for results of NOVA-Server...") result = thread.get() return str(result) diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index c5ce214..80121d0 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -25,16 +25,18 @@ class TextExtractionPDF(DVMTaskInterface): TASK: str = "pdf-to-text" COST: int = 0 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): self.NAME = name - dvm_config.SUPPORTED_TASKS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + def is_input_supported(self, input_type, input_content): if input_type != "url" and input_type != "event": diff --git a/tasks/translation.py b/tasks/translation.py index 1511fbe..9596927 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -23,16 +23,18 @@ class Translation(DVMTaskInterface): TASK: str = "translation" COST: int = 0 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): self.NAME = name - dvm_config.SUPPORTED_TASKS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": diff --git a/test_client.py b/test_dvm_client.py similarity index 95% rename from test_client.py rename to test_dvm_client.py index 3083c96..8455a28 100644 --- a/test_client.py +++ b/test_dvm_client.py @@ -11,12 +11,10 @@ from utils.dvmconfig import DVMConfig from utils.nostr_utils import send_event from utils.definitions import EventDefinitions -import utils.env as env - # TODO HINT: Best use this path with a previously whitelisted privkey, as zapping events is not implemented in the lib/code def nostr_client_test_translation(input, kind, lang, sats, satsmax): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) if kind == "text": iTag = Tag.parse(["i", input, "text"]) elif kind == "event": @@ -43,7 +41,7 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax): def nostr_client_test_image(prompt): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) iTag = Tag.parse(["i", prompt, "text"]) outTag = Tag.parse(["output", "image/png;format=url"]) @@ -69,7 +67,7 @@ def nostr_client_test_image(prompt): return event.as_json() def nostr_client(): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) sk = keys.secret_key() pk = keys.public_key() print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") diff --git a/utils/admin_utils.py b/utils/admin_utils.py index 5063418..bde1110 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -10,7 +10,7 @@ from utils.nip89_utils import nip89_announce_tasks from utils.nostr_utils import send_event class AdminConfig: - REBROADCASTNIP89: bool = False + REBROADCAST_NIP89: bool = False WHITELISTUSER: bool = False UNWHITELISTUSER: bool = False BLACKLISTUSER: bool = False @@ -34,7 +34,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC db = dvmconfig.DB - rebroadcast_nip89 = adminconfig.REBROADCASTNIP89 + rebroadcast_nip89 = adminconfig.REBROADCAST_NIP89 cleandb = adminconfig.ClEANDB listdatabase = adminconfig.LISTDATABASE deleteuser = adminconfig.DELETEUSER diff --git a/utils/database_utils.py b/utils/database_utils.py index 47ba275..01f7c68 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -220,8 +220,8 @@ def fetch_user_metadata(sender, client) -> (str, str, str): name = metadata.get_display_name() if str(name) == "" or name is None: name = metadata.get_name() - nip05 = metadata.get_nip05() - lud16 = metadata.get_lud16() + nip05 = metadata.get_nip05() + lud16 = metadata.get_lud16() except: print("Couldn't get meta information") diff --git a/utils/definitions.py b/utils/definitions.py index 666868e..ceed65e 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -3,13 +3,9 @@ from dataclasses import dataclass from nostr_sdk import Event -from utils import env - NEW_USER_BALANCE: int = 250 # Free credits for new users - - class EventDefinitions: KIND_DM: int = 4 KIND_ZAP: int = 9735 @@ -41,8 +37,6 @@ class EventDefinitions: KIND_NIP90_RESULT_GENERIC] - - @dataclass class JobToWatch: event_id: str diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 74fd5a2..5e16096 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -1,12 +1,11 @@ import os -from utils import env from utils.nip89_utils import NIP89Announcement class DVMConfig: SUPPORTED_TASKS = [] - PRIVATE_KEY: str = os.getenv(env.NOSTR_PRIVATE_KEY) + PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", @@ -17,6 +16,7 @@ class DVMConfig: LNBITS_URL = 'https://lnbits.com' DB: str NIP89: NIP89Announcement + DM_ALLOWED = [] REQUIRES_NIP05: bool = False SHOW_RESULT_BEFORE_PAYMENT: bool = True # if this is true show results even when not paid right after autoprocess diff --git a/utils/env.py b/utils/env.py deleted file mode 100644 index 6945dfd..0000000 --- a/utils/env.py +++ /dev/null @@ -1,14 +0,0 @@ -NOSTR_PRIVATE_KEY = "NOSTR_PRIVATE_KEY" -NOSTR_TEST_CLIENT_PRIVATE_KEY = "NOSTR_TEST_CLIENT_PRIVATE_KEY" - -USER_DB_PATH = "USER_DB_PATH" - -LNBITS_INVOICE_KEY = "LNBITS_INVOICE_KEY" -LNBITS_HOST = "LNBITS_HOST" - -TASK_TRANSLATION_NIP89_DTAG = "TASK_TRANSLATION_NIP89_DTAG" -TASK_TEXTEXTRACTION_NIP89_DTAG = "TASK_TEXTEXTRACTION_NIP89_DTAG" -TASK_IMAGEGENERATION_NIP89_DTAG = "TASK_IMAGEGENERATION_NIP89_DTAG" -TASK_IMAGEGENERATION_NIP89_DTAG2 = "TASK_IMAGEGENERATION_NIP89_DTAG2" - -