diff --git a/README.md b/README.md index 5f0c8cb1..27d4404c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,27 @@ # AI Toolkit by Ostris -## Support my work +## Support My Work - -Patreon - ostris - +If you enjoy my work, or use it for commercial purposes, please consider sponsoring me so I can continue to maintain it. Every bit helps! + +[Become a sponsor on GitHub](https://github.com/orgs/ostris) or [support me on Patreon](https://www.patreon.com/ostris). + +Thank you to all my current supporters! + +_Last updated: 2025-03-02_ + +## GitHub Sponsors + +Replicate + +## Patreon Supporters + +Al H clement Delangue Cosmosis David Garrido Doron Adler Eli Slugworth EmmanuelMr18 HestoySeghuro . Jack Blakely Jason Jean-Tristan Marin Jodh Singh John Dopamine Joseph Rocca Kristjan Retter Maciej Popławski Michael Levine Miguel Lara Misch Strotz Mohamed Oumoumad Noctre Patron Paul Fidika Prasanth Veerina Razvan Grigore Steve Hanff Steve Informal The Local Lab Vladimir Sotnikov Zoltán-Csaba Nyiró + + +--- -I work on open source full time, which means I 100% rely on donations to make a living. If you find this project helpful, or use it in for commercial purposes, please consider donating to support my work on [Patreon](https://www.patreon.com/ostris) or [Github Sponsors](https://github.com/sponsors/ostris). ## Installation diff --git a/scripts/update_sponsors.py b/scripts/update_sponsors.py new file mode 100644 index 00000000..1e3bbf63 --- /dev/null +++ b/scripts/update_sponsors.py @@ -0,0 +1,309 @@ +import os +import requests +import json +from datetime import datetime +from dotenv import load_dotenv + +# Load environment variables from .env file +env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env") +load_dotenv(dotenv_path=env_path) + +# API credentials +PATREON_TOKEN = os.getenv("PATREON_ACCESS_TOKEN") +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +GITHUB_USERNAME = os.getenv("GITHUB_USERNAME") +GITHUB_ORG = os.getenv("GITHUB_ORG") # Organization name (optional) + +# Output file +README_PATH = "SUPPORTERS.md" + +def fetch_patreon_supporters(): + """Fetch current Patreon supporters""" + print("Fetching Patreon supporters...") + + headers = { + "Authorization": f"Bearer {PATREON_TOKEN}", + "Content-Type": "application/json" + } + + url = "https://www.patreon.com/api/oauth2/v2/campaigns" + + try: + # First get the campaign ID + campaign_response = requests.get(url, headers=headers) + campaign_response.raise_for_status() + campaign_data = campaign_response.json() + + if not campaign_data.get('data'): + print("No campaigns found for this Patreon account") + return [] + + campaign_id = campaign_data['data'][0]['id'] + + # Now get the supporters for this campaign + members_url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{campaign_id}/members" + params = { + "include": "user", + "fields[member]": "full_name,is_follower,patron_status", # Removed profile_url + "fields[user]": "image_url" + } + + supporters = [] + while members_url: + members_response = requests.get(members_url, headers=headers, params=params) + members_response.raise_for_status() + members_data = members_response.json() + + # Process the response to extract active patrons + for member in members_data.get('data', []): + attributes = member.get('attributes', {}) + + # Only include active patrons + if attributes.get('patron_status') == 'active_patron': + name = attributes.get('full_name', 'Anonymous Supporter') + + # Get user data which contains the profile image + user_id = member.get('relationships', {}).get('user', {}).get('data', {}).get('id') + profile_image = None + profile_url = None # Removed profile_url since it's not supported + + if user_id: + for included in members_data.get('included', []): + if included.get('id') == user_id and included.get('type') == 'user': + profile_image = included.get('attributes', {}).get('image_url') + break + + supporters.append({ + 'name': name, + 'profile_image': profile_image, + 'profile_url': profile_url, # This will be None + 'platform': 'Patreon', + 'amount': 0 # Placeholder, as Patreon API doesn't provide this in the current response + }) + + # Handle pagination + members_url = members_data.get('links', {}).get('next') + + print(f"Found {len(supporters)} active Patreon supporters") + return supporters + + except requests.exceptions.RequestException as e: + print(f"Error fetching Patreon data: {e}") + print(f"Response content: {e.response.content if hasattr(e, 'response') else 'No response content'}") + return [] + +def fetch_github_sponsors(): + """Fetch current GitHub sponsors for a user or organization""" + print("Fetching GitHub sponsors...") + + headers = { + "Authorization": f"Bearer {GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3+json" + } + + # Determine if we're fetching for a user or an organization + entity_type = "organization" if GITHUB_ORG else "user" + entity_name = GITHUB_ORG if GITHUB_ORG else GITHUB_USERNAME + + if not entity_name: + print("Error: Neither GITHUB_USERNAME nor GITHUB_ORG is set") + return [] + + # Different GraphQL query structure based on entity type + if entity_type == "user": + query = """ + query { + user(login: "%s") { + sponsorshipsAsMaintainer(first: 100) { + nodes { + sponsorEntity { + ... on User { + login + name + avatarUrl + url + } + ... on Organization { + login + name + avatarUrl + url + } + } + tier { + monthlyPriceInDollars + } + isOneTimePayment + isActive + } + } + } + } + """ % entity_name + else: # organization + query = """ + query { + organization(login: "%s") { + sponsorshipsAsMaintainer(first: 100) { + nodes { + sponsorEntity { + ... on User { + login + name + avatarUrl + url + } + ... on Organization { + login + name + avatarUrl + url + } + } + tier { + monthlyPriceInDollars + } + isOneTimePayment + isActive + } + } + } + } + """ % entity_name + + try: + response = requests.post( + "https://api.github.com/graphql", + headers=headers, + json={"query": query} + ) + response.raise_for_status() + data = response.json() + + # Process the response - the path to the data differs based on entity type + if entity_type == "user": + sponsors_data = data.get('data', {}).get('user', {}).get('sponsorshipsAsMaintainer', {}).get('nodes', []) + else: + sponsors_data = data.get('data', {}).get('organization', {}).get('sponsorshipsAsMaintainer', {}).get('nodes', []) + + sponsors = [] + for sponsor in sponsors_data: + # Only include active sponsors + if sponsor.get('isActive'): + entity = sponsor.get('sponsorEntity', {}) + name = entity.get('name') or entity.get('login', 'Anonymous Sponsor') + profile_image = entity.get('avatarUrl') + profile_url = entity.get('url') + amount = sponsor.get('tier', {}).get('monthlyPriceInDollars', 0) + + sponsors.append({ + 'name': name, + 'profile_image': profile_image, + 'profile_url': profile_url, + 'platform': 'GitHub Sponsors', + 'amount': amount + }) + + print(f"Found {len(sponsors)} active GitHub sponsors for {entity_type} '{entity_name}'") + return sponsors + + except requests.exceptions.RequestException as e: + print(f"Error fetching GitHub sponsors data: {e}") + return [] + +def generate_readme(supporters): + """Generate a README.md file with supporter information""" + print(f"Generating {README_PATH}...") + + # Sort supporters by amount (descending) and then by name + supporters.sort(key=lambda x: (-x['amount'], x['name'].lower())) + + # Determine the proper footer links based on what's configured + github_entity = GITHUB_ORG if GITHUB_ORG else GITHUB_USERNAME + github_entity_type = "orgs" if GITHUB_ORG else "sponsors" + github_sponsor_url = f"https://github.com/{github_entity_type}/{github_entity}" + + with open(README_PATH, "w", encoding="utf-8") as f: + f.write("## Support My Work\n\n") + f.write("If you enjoy my work, or use it for commercial purposes, please consider sponsoring me so I can continue to maintain it. Every bit helps! \n\n") + # Create appropriate call-to-action based on what's configured + cta_parts = [] + if github_entity: + cta_parts.append(f"[Become a sponsor on GitHub]({github_sponsor_url})") + if PATREON_TOKEN: + cta_parts.append("[support me on Patreon](https://www.patreon.com/ostris)") + + if cta_parts: + if GITHUB_ORG: + f.write(f"{' or '.join(cta_parts)}.\n\n") + f.write("Thank you to all my current supporters!\n\n") + + f.write(f"_Last updated: {datetime.now().strftime('%Y-%m-%d')}_\n\n") + + # Write GitHub Sponsors section + github_sponsors = [s for s in supporters if s['platform'] == 'GitHub Sponsors'] + if github_sponsors: + f.write("### GitHub Sponsors\n\n") + for sponsor in github_sponsors: + if sponsor['profile_image']: + f.write(f"\"{sponsor['name']}\" ") + else: + f.write(f"[{sponsor['name']}]({sponsor['profile_url']}) ") + f.write("\n\n") + + # Write Patreon section + patreon_supporters = [s for s in supporters if s['platform'] == 'Patreon'] + if patreon_supporters: + f.write("### Patreon Supporters\n\n") + for supporter in patreon_supporters: + if supporter['profile_image']: + f.write(f"\"{supporter['name']}\" ") + else: + f.write(f"[{supporter['name']}]({supporter['profile_url']}) ") + f.write("\n\n") + + f.write("\n---\n\n") + + + print(f"Successfully generated {README_PATH} with {len(supporters)} supporters!") + +def main(): + """Main function""" + print("Starting supporter data collection...") + + # Check if required environment variables are set + missing_vars = [] + if not GITHUB_TOKEN: + missing_vars.append("GITHUB_TOKEN") + + # Either username or org is required for GitHub + if not GITHUB_USERNAME and not GITHUB_ORG: + missing_vars.append("GITHUB_USERNAME or GITHUB_ORG") + + # Patreon token is optional but warn if missing + patreon_enabled = bool(PATREON_TOKEN) + + if missing_vars: + print(f"Error: Missing required environment variables: {', '.join(missing_vars)}") + print("Please add them to your .env file") + return + + if not patreon_enabled: + print("Warning: PATREON_ACCESS_TOKEN not set. Will only fetch GitHub sponsors.") + + # Fetch data from both platforms + patreon_supporters = fetch_patreon_supporters() if PATREON_TOKEN else [] + github_sponsors = fetch_github_sponsors() + + # Combine supporters from both platforms + all_supporters = patreon_supporters + github_sponsors + + if not all_supporters: + print("No supporters found on either platform") + return + + # Generate README + generate_readme(all_supporters) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/todo_multigpu.md b/todo_multigpu.md deleted file mode 100644 index 02d5abda..00000000 --- a/todo_multigpu.md +++ /dev/null @@ -1,3 +0,0 @@ -- only do ema on main device? shouldne be needed other than saving and sampling -- check when to unwrap model and what it does -- disable timer for non main local \ No newline at end of file