Track deploy, social, and Reaper scripts

Scripts already referenced from CLAUDE.md but never committed: deploy_searxng.sh
(which stands up the SearXNG instance Devon's web_search depends on),
post_milestone.py, generate_milestone_images.py, make_x_launch_assets.py,
schedule_x_launch.py, download_music.py, and the Reaper bleep-selection script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 04:23:53 -05:00
co-authored by Claude Opus 5
parent 2071ae4181
commit 9d49b013c2
7 changed files with 2011 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""Post 1000 downloads milestone to all social platforms via Postiz."""
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
import requests
# Load .env
env_path = Path(__file__).parent / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
POSTIZ_API_KEY = os.getenv("POSTIZ_API_KEY")
POSTIZ_URL = os.getenv("POSTIZ_URL", "https://social.lukeattheroost.com")
IMAGE_PATH = Path(__file__).parent / "social_posts" / "1000_milestone" / "main_1000_celebration.jpg"
def get_api_url(path: str) -> str:
return f"{POSTIZ_URL.rstrip('/')}/api/public/v1{path}"
def api_headers() -> dict:
return {"Authorization": POSTIZ_API_KEY, "Content-Type": "application/json"}
def fetch_integrations() -> list[dict]:
resp = requests.get(get_api_url("/integrations"), headers=api_headers(), timeout=15)
if resp.status_code != 200:
print(f"Error fetching integrations: {resp.status_code} {resp.text[:200]}")
sys.exit(1)
return resp.json()
def find_integration(integrations: list[dict], provider: str) -> dict | None:
for integ in integrations:
if integ.get("identifier", "").startswith(provider) and not integ.get("disabled"):
return integ
return None
def upload_image(file_path: Path) -> dict:
headers = {"Authorization": POSTIZ_API_KEY}
mime = "image/jpeg" if file_path.suffix.lower() in (".jpg", ".jpeg") else "image/png"
with open(file_path, "rb") as f:
resp = requests.post(
get_api_url("/upload"),
headers=headers,
files={"file": (file_path.name, f, mime)},
timeout=60,
)
if resp.status_code not in (200, 201):
print(f"Upload failed: {resp.status_code} {resp.text[:200]}")
return {}
return resp.json()
def create_post(integration_id: str, content: str, media: dict, settings: dict) -> dict:
date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
payload = {
"type": "now",
"date": date,
"shortLink": False,
"tags": [],
"posts": [
{
"integration": {"id": integration_id},
"value": [{"content": content, "image": [media] if media else []}],
"settings": settings,
}
],
}
resp = requests.post(get_api_url("/posts"), headers=api_headers(), json=payload, timeout=30)
if resp.status_code not in (200, 201):
print(f" Post failed: {resp.status_code} {resp.text[:300]}")
return {}
return resp.json()
# --- Post content per platform ---
POSTS = {
"instagram": {
"content": """1,000 downloads in one month. 🎙️
27 episodes. 200+ callers. 111 unique characters. 13 returning regulars.
Luke at the Roost is a late-night call-in show where AI-generated characters phone in with their problems — relationship drama, moral dilemmas, conspiracy theories, drunk confessions — and I give them real advice, live.
Every caller has a unique voice, a backstory, and a reason for calling. Some of them keep calling back.
Thank you to everyone who's tuned in. This thing was supposed to be a weird experiment. Now it's a weird experiment that 1,000 people have listened to.
New episodes daily. Link in bio.
#podcast #ai #artificialintelligence #sideproject #indieproject #podcastlife #latenightradio #callinshow #milestone #1000downloads""",
"settings": {"__type": "instagram", "post_type": "post", "collaborators": []},
},
"facebook": {
"content": """🎙️ MILESTONE: 1,000 Downloads
One month ago I launched a weird experiment — a late-night call-in radio show where AI-generated characters phone in with their problems, and I give them real advice.
27 episodes later, 200+ callers have phoned in. Some of them keep calling back. Listeners have favorites. People genuinely care about what happens to these characters.
Thank you to every single person who gave this a listen. It started as a side project and it's become something I look forward to every day.
Listen free: lukeattheroost.com
Call in for real: 208-439-LUKE""",
"settings": {"__type": "facebook"},
},
"threads": {
"content": """1,000 downloads in one month. 🎙️
27 episodes. 200+ callers. 111 unique characters. 13 returning regulars.
Luke at the Roost is a late-night call-in show where AI-generated characters phone in with their problems and I give them real advice, live.
Thank you to everyone who's tuned in. This weird experiment just hit a milestone.
lukeattheroost.com
#podcast #ai #sideproject #1000downloads""",
"settings": {"__type": "threads"},
},
"linkedin": {
"content": """1,000 downloads in 30 days — here's what I learned building an AI radio show
A month ago I launched Luke at the Roost, a late-night call-in radio show where every caller is an AI-generated character. I'm the host. They phone in with problems. I give them advice. Every conversation is improvised.
27 episodes and 200+ callers later, the show just hit 1,000 downloads.
Some things that surprised me:
People connect with AI characters. Listeners have favorites. They ask about regulars by name. When a caller's story evolves across episodes, people notice and care. The characters aren't real, but the emotional engagement is.
Constraints drive creativity. Each caller gets a token budget based on their personality type. Emotional callers get more room to ramble. Gossip callers are quick and punchy. This artificial constraint mirrors how real people actually talk — and it makes every call feel distinct.
The tech is the easy part. LLMs, voice synthesis, audio routing — that's engineering. The hard part is being a good host. Knowing when to push, when to listen, when to make a joke. AI handles the callers. The human skill is the conversation.
The full technical breakdown: lukeattheroost.com/how-it-works
Listen: lukeattheroost.com
Thank you to everyone who gave this weird experiment a chance.""",
"settings": {"__type": "linkedin"},
},
"mastodon": {
"content": """1,000 downloads. 27 episodes. 200+ AI callers given advice on everything from breakups to fish consciousness.
Luke at the Roost hit a milestone today and I just want to say thank you to everyone who's been listening.
This whole thing is self-hosted end-to-end — Castopod on a QNAP NAS, Cloudflare CDN, custom Python pipeline for recording, post-production, and publishing. No big platforms in the loop.
If you haven't heard it: it's a late-night call-in show. AI characters phone in. I talk to them live. It's improvised, weird, and somehow heartfelt.
https://lukeattheroost.com""",
"settings": {"__type": "mastodon"},
},
"tiktok": {
"content": """1,000 downloads in one month 🎙️
27 episodes. 200+ AI callers. 13 returning regulars.
Luke at the Roost — a late-night call-in show where AI characters phone in with their problems and I give them real advice, live.
Thank you to everyone listening.
#podcast #ai #artificialintelligence #sideproject #latenightradio #callinshow #1000downloads""",
"settings": {
"__type": "tiktok",
"privacy_level": "PUBLIC_TO_EVERYONE",
"duet": False,
"stitch": False,
"comment": True,
"autoAddMusic": "no",
"brand_content_toggle": False,
"brand_organic_toggle": False,
"content_posting_method": "DIRECT_POST",
},
},
"nostr": {
"content": """1,000 downloads. 27 episodes. 200+ AI callers given advice.
Luke at the Roost just hit a milestone. Thank you to everyone listening.
It's a late-night call-in show where AI-generated characters phone in with their problems and I give them real advice, live. Every conversation is improvised.
https://lukeattheroost.com""",
"settings": {"__type": "nostr"},
},
}
def main():
dry_run = "--dry-run" in sys.argv
if not POSTIZ_API_KEY:
print("Error: POSTIZ_API_KEY not set")
sys.exit(1)
if not IMAGE_PATH.exists():
print(f"Error: Image not found at {IMAGE_PATH}")
sys.exit(1)
print("Fetching connected accounts from Postiz...")
integrations = fetch_integrations()
available = {}
for platform in POSTS:
integ = find_integration(integrations, platform)
if integ:
available[platform] = integ
print(f"{platform}: {integ.get('name', 'connected')}")
else:
print(f"{platform}: not connected, skipping")
if not available:
print("\nNo platforms available!")
sys.exit(1)
print(f"\nWill post to {len(available)} platform(s) with image: {IMAGE_PATH.name}")
if dry_run:
print("\n--- DRY RUN ---")
for platform in available:
print(f"\n[{platform.upper()}]")
print(POSTS[platform]["content"][:200] + "...")
print("\nDry run complete — nothing posted.")
return
# Upload image once
print(f"\nUploading image...")
media = upload_image(IMAGE_PATH)
if not media:
print("Failed to upload image, aborting")
sys.exit(1)
print(f" Uploaded: {media.get('path', 'ok')}")
# Post to each platform
results = {}
for platform, integ in available.items():
post_data = POSTS[platform]
print(f"\nPosting to {platform}...")
result = create_post(integ["id"], post_data["content"], media, post_data["settings"])
if result:
print(f"{platform}: Posted!")
results[platform] = True
else:
print(f"{platform}: Failed")
results[platform] = False
# Summary
succeeded = [p for p, ok in results.items() if ok]
failed = [p for p, ok in results.items() if not ok]
print(f"\n{'='*40}")
print(f"Posted to {len(succeeded)}/{len(results)} platforms")
if succeeded:
print(f"{', '.join(succeeded)}")
if failed:
print(f"{', '.join(failed)}")
if __name__ == "__main__":
main()