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
-
-
-
+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
+
+
+
+## Patreon Supporters
+
+
+
+
+---
-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"
")
+ 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"
")
+ 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