diff --git a/.env_example b/.env_example index ff9802a..098f9d9 100644 --- a/.env_example +++ b/.env_example @@ -18,6 +18,7 @@ REPLICATE_API_TOKEN = "" #API Key to run models on replicate.com HUGGINGFACE_EMAIL = "" HUGGINGFACE_PASSWORD = "" COINSTATSOPENAPI_KEY = "" +NOSTR_BUILD_ACCOUNT_PK = "" # Enter the private key of an account you use with nostr.build # We will automatically create dtags and private keys based on the identifier variable in main. # If your DVM already has a dtag and private key you can replace it here before publishing the DTAG to not create a new one. diff --git a/nostr_dvm/backends/nova_server/utils.py b/nostr_dvm/backends/nova_server/utils.py index 15c224a..77e73b0 100644 --- a/nostr_dvm/backends/nova_server/utils.py +++ b/nostr_dvm/backends/nova_server/utils.py @@ -88,7 +88,7 @@ def check_server_status(jobID, address) -> str | pd.DataFrame: if content_type == "image/jpeg": image = Image.open(io.BytesIO(response.content)) image.save("./outputs/image.jpg") - result = upload_media_to_hoster("./outputs/image.jpg") + result = asyncio.run(upload_media_to_hoster("./outputs/image.jpg")) os.remove("./outputs/image.jpg") return result elif content_type == 'video/mp4': @@ -97,7 +97,7 @@ def check_server_status(jobID, address) -> str | pd.DataFrame: f.close() clip = VideoFileClip("./outputs/video.mp4") clip.write_videofile("./outputs/video2.mp4") - result = upload_media_to_hoster("./outputs/video2.mp4") + result = asyncio.run(upload_media_to_hoster("./outputs/video2.mp4")) clip.close() os.remove("./outputs/video.mp4") os.remove("./outputs/video2.mp4") diff --git a/nostr_dvm/tasks/convert_media.py b/nostr_dvm/tasks/convert_media.py index 0156285..ab493d0 100644 --- a/nostr_dvm/tasks/convert_media.py +++ b/nostr_dvm/tasks/convert_media.py @@ -72,7 +72,7 @@ class MediaConverter(DVMTaskInterface): async def process(self, request_form): options = self.set_options(request_form) - url = upload_media_to_hoster(options["filepath"]) + url = await upload_media_to_hoster(options["filepath"]) return url diff --git a/nostr_dvm/tasks/imagegeneration_openai_dalle.py b/nostr_dvm/tasks/imagegeneration_openai_dalle.py index 5516de1..1ec328b 100644 --- a/nostr_dvm/tasks/imagegeneration_openai_dalle.py +++ b/nostr_dvm/tasks/imagegeneration_openai_dalle.py @@ -112,7 +112,7 @@ class ImageGenerationDALLE(DVMTaskInterface): response = requests.get(image_url) image = Image.open(BytesIO(response.content)).convert("RGB") image.save("./outputs/image.jpg") - result = upload_media_to_hoster("./outputs/image.jpg") + result = await upload_media_to_hoster("./outputs/image.jpg") return result except Exception as e: diff --git a/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py b/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py index 7b60a76..96724aa 100644 --- a/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py +++ b/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py @@ -107,7 +107,7 @@ class ImageGenerationReplicateSDXL(DVMTaskInterface): response = requests.get(output[0]) image = Image.open(BytesIO(response.content)).convert("RGB") image.save("./outputs/image.jpg") - result = upload_media_to_hoster("./outputs/image.jpg") + result = await upload_media_to_hoster("./outputs/image.jpg") return result except Exception as e: diff --git a/nostr_dvm/tasks/imagegeneration_sd21_mlx.py b/nostr_dvm/tasks/imagegeneration_sd21_mlx.py index c7e9e7d..04da1d5 100644 --- a/nostr_dvm/tasks/imagegeneration_sd21_mlx.py +++ b/nostr_dvm/tasks/imagegeneration_sd21_mlx.py @@ -136,7 +136,7 @@ class ImageGenerationMLX(DVMTaskInterface): # Save them to disc image = Image.fromarray(x.__array__()) image.save("./outputs/image.jpg") - result = upload_media_to_hoster("./outputs/image.jpg") + result = await upload_media_to_hoster("./outputs/image.jpg") return result except Exception as e: diff --git a/nostr_dvm/tasks/texttospeech.py b/nostr_dvm/tasks/texttospeech.py index a6e0df0..6484b35 100644 --- a/nostr_dvm/tasks/texttospeech.py +++ b/nostr_dvm/tasks/texttospeech.py @@ -1,8 +1,10 @@ import json import os +import ffmpegio from nostr_sdk import Kind +from nostr_dvm.utils.mediasource_utils import organize_input_media_data from nostr_dvm.utils.nip88_utils import NIP88Config os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" @@ -37,6 +39,7 @@ class TextToSpeech(DVMTaskInterface): async def init_dvm(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) + self.dvm_config = dvm_config async def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: @@ -132,8 +135,14 @@ class TextToSpeech(DVMTaskInterface): tts.tts_to_file( text=text_clean, - speaker_wav=options["input_wav"], language=options["language"], file_path="outputs/output.mp3") - result = upload_media_to_hoster("outputs/output.mp3") + speaker_wav=options["input_wav"], language=options["language"], file_path="outputs/output.wav") + + print("Converting Audio") + final_filename = "outputs/output.mp3" + fs, x = ffmpegio.audio.read("outputs/output.wav", sample_fmt='dbl', ac=1) + ffmpegio.audio.write(final_filename, fs, x, overwrite=True) + + result = await upload_media_to_hoster(final_filename, dvm_config=self.dvm_config) print(result) return result except Exception as e: diff --git a/nostr_dvm/tasks/videogeneration_replicate_svd.py b/nostr_dvm/tasks/videogeneration_replicate_svd.py index 3e196ca..7893092 100644 --- a/nostr_dvm/tasks/videogeneration_replicate_svd.py +++ b/nostr_dvm/tasks/videogeneration_replicate_svd.py @@ -99,7 +99,7 @@ class VideoGenerationReplicateSVD(DVMTaskInterface): print(output) urllib.request.urlretrieve(output, "./outputs/svd.mp4") - result = upload_media_to_hoster("./outputs/svd.mp4") + result = await upload_media_to_hoster("./outputs/svd.mp4") return result except Exception as e: diff --git a/nostr_dvm/utils/dvmconfig.py b/nostr_dvm/utils/dvmconfig.py index 965558e..7137dd4 100644 --- a/nostr_dvm/utils/dvmconfig.py +++ b/nostr_dvm/utils/dvmconfig.py @@ -6,7 +6,6 @@ from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config from nostr_dvm.utils.nostr_utils import check_and_set_private_key from nostr_dvm.utils.outbox_utils import AVOID_OUTBOX_RELAY_LIST -from nostr_dvm.utils.output_utils import PostProcessFunctionType from nostr_dvm.utils.zap_utils import check_and_set_ln_bits_keys class DVMConfig: @@ -31,7 +30,7 @@ class DVMConfig: RELAY_TIMEOUT = 5 RELAY_LONG_TIMEOUT = 30 - EXTERNAL_POST_PROCESS_TYPE = PostProcessFunctionType.NONE # Leave this on None, except the DVM is external + EXTERNAL_POST_PROCESS_TYPE = 0 # Leave this on None, except the DVM is external LNBITS_INVOICE_KEY = '' # Will all automatically generated by default, or read from .env LNBITS_ADMIN_KEY = '' # In order to pay invoices, e.g. from the bot to DVMs, or reimburse users. LNBITS_URL = 'https://lnbits.com' @@ -42,8 +41,8 @@ class DVMConfig: DB: str NEW_USER_BALANCE: int = 0 # Free credits for new users SUBSCRIPTION_MANAGEMENT = 'https://noogle.lol/discovery' - NIP88: NIP88Config - NIP89: NIP89Config + NIP88: NIP88Config = NIP88Config() + NIP89: NIP89Config = NIP89Config() SEND_FEEDBACK_EVENTS = True SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess SCHEDULE_UPDATES_SECONDS = 0 diff --git a/nostr_dvm/utils/nip89_utils.py b/nostr_dvm/utils/nip89_utils.py index 326bf9b..bcb6b66 100644 --- a/nostr_dvm/utils/nip89_utils.py +++ b/nostr_dvm/utils/nip89_utils.py @@ -31,9 +31,9 @@ async def nip89_announce_tasks(dvm_config, client): keys = Keys.parse(dvm_config.NIP89.PK) content = dvm_config.NIP89.CONTENT event = EventBuilder(EventDefinitions.KIND_ANNOUNCEMENT, content, [k_tag, d_tag]).to_event(keys) - evenid = await send_event(event, client=client, dvm_config=dvm_config) + eventid = await send_event(event, client=client, dvm_config=dvm_config) - print(bcolors.BLUE + "[" + dvm_config.NIP89.NAME + "] Announced NIP 89 for " + dvm_config.NIP89.NAME +" (" + evenid.to_hex() +")" + bcolors.ENDC) + print(bcolors.BLUE + "[" + dvm_config.NIP89.NAME + "] Announced NIP 89 for " + dvm_config.NIP89.NAME +" (" + eventid.to_hex() +")" + bcolors.ENDC) diff --git a/nostr_dvm/utils/nip98_utils.py b/nostr_dvm/utils/nip98_utils.py new file mode 100644 index 0000000..0ef4866 --- /dev/null +++ b/nostr_dvm/utils/nip98_utils.py @@ -0,0 +1,20 @@ +import base64 +import hashlib +from nostr_sdk import EventBuilder, Tag, Kind, Keys + + +def sha256sum(filename): + with open(filename, 'rb', buffering=0) as f: + return hashlib.file_digest(f, 'sha256').hexdigest() + +async def generate_nip98_header(filepath, dvm_config): + keys = Keys.parse(dvm_config.NIP89.PK) + utag = Tag.parse(["u", "https://nostr.build/api/v2/upload/files"]) + methodtag = Tag.parse(["method", "POST"]) + payloadtag = Tag.parse(["payload", sha256sum(filepath)]) + event = EventBuilder(Kind(27235), "", [utag, methodtag, payloadtag]).to_event(keys) + + encoded_nip98_event = base64.b64encode(event.as_json().encode('utf-8')).decode('utf-8') + + return "Nostr " + encoded_nip98_event + diff --git a/nostr_dvm/utils/output_utils.py b/nostr_dvm/utils/output_utils.py index 845fd7d..201e310 100644 --- a/nostr_dvm/utils/output_utils.py +++ b/nostr_dvm/utils/output_utils.py @@ -6,11 +6,14 @@ from types import NoneType import emoji import requests -from nostr_sdk import Tag, PublicKey, EventId +from nostr_sdk import Tag, PublicKey, EventId, Keys from pyupload.uploader import CatboxUploader import pandas +from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nip98_utils import generate_nip98_header + ''' Post process results to either given output format or a Nostr readable plain text. ''' @@ -146,26 +149,35 @@ Will probably need to switch to another system in the future. ''' -def upload_media_to_hoster(filepath: str): +async def upload_media_to_hoster(filepath: str, dvm_config=None, usealternativeforlargefiles=True): + + if dvm_config is None: + dvm_config = DVMConfig() + dvm_config.NIP89.PK = Keys.parse(os.getenv("NOSTR_BUILD_ACCOUNT_PK")).secret_key().to_hex() + print("Uploading image: " + filepath) try: files = {'file': open(filepath, 'rb')} file_stats = os.stat(filepath) sizeinmb = file_stats.st_size / (1024 * 1024) print("Filesize of Uploaded media: " + str(sizeinmb) + " Mb.") - if sizeinmb > 25: + if sizeinmb > 25 and usealternativeforlargefiles: print("Filesize over Nostr.build limited, using catbox") uploader = CatboxUploader(filepath) result = uploader.execute() return result else: url = 'https://nostr.build/api/v2/upload/files' - response = requests.post(url, files=files) + auth = await generate_nip98_header(filepath, dvm_config) + headers = {'authorization': auth} + + response = requests.post(url, files=files, headers=headers) print(response.text) json_object = json.loads(response.text) result = json_object["data"][0]["url"] return result except Exception as e: + print(e) try: uploader = CatboxUploader(filepath) result = uploader.execute() diff --git a/setup.py b/setup.py index 157c403..ff60b85 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = '0.6.29' +VERSION = '0.6.30' DESCRIPTION = 'A framework to build and run Nostr NIP90 Data Vending Machines' LONG_DESCRIPTION = ('A framework to build and run Nostr NIP90 Data Vending Machines. See the github repository for more information') diff --git a/tests/tts.py b/tests/tts.py index 707c475..6a35266 100644 --- a/tests/tts.py +++ b/tests/tts.py @@ -22,7 +22,7 @@ if __name__ == '__main__': dvm_config = build_default_config(identifier) dvm_config.USE_OWN_VENV = True dvm_config.FIX_COST = 0 - dvm_config.PER_UNIT_COST = 0.2 + dvm_config.PER_UNIT_COST = 0.1 admin_config_tts.LUD16 = dvm_config.LN_ADDRESS # use an alternative local wav file you want to use for cloning options = {'input_file': ""} diff --git a/tests/upload_hoster.py b/tests/upload_hoster.py index 0d3725e..15d04f6 100644 --- a/tests/upload_hoster.py +++ b/tests/upload_hoster.py @@ -1,3 +1,21 @@ +import asyncio +import os +from pathlib import Path + +import dotenv +from nostr_sdk import Keys + + from nostr_dvm.utils.output_utils import upload_media_to_hoster -upload_media_to_hoster("tests/output.wav") \ No newline at end of file +if __name__ == '__main__': + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + + + asyncio.run(upload_media_to_hoster("tests/output.wav")) + # asyncio.run(upload_media_to_hoster("tests/test.jpeg", dvm_config))