mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-03-17 21:31:52 +01:00
simplifiying code, adding a basic bot functionality
This commit is contained in:
parent
dc727021f6
commit
8a7e3168f5
21
.env_example
21
.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"
|
@ -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)
|
||||
|
||||
|
160
bot.py
Normal file
160
bot.py
Normal file
@ -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)
|
57
dvm.py
57
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)
|
||||
|
||||
|
@ -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
|
||||
|
109
main.py
109
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__':
|
||||
|
129
playground.py
Normal file
129
playground.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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":
|
||||
|
@ -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":
|
||||
|
@ -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()} ")
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
14
utils/env.py
14
utils/env.py
@ -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"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user