@m13v/s4l 1.6.197-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1336 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +513 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # Download + transcribe every IG post for one creator.
3
+ # Usage: ./ig_batch_creator.sh <ig_handle>
4
+ #
5
+ # Reads URLs from scripts/ig_creators_run/<handle>/urls.txt (one post URL per line).
6
+ # Writes <shortcode>.{mp4,m4a,info.json,deepgram.json} into the same dir.
7
+ # Idempotent: skips a post if both .mp4 and .deepgram.json already exist.
8
+ # Stops the loop after N consecutive download failures (likely IG rate-limit).
9
+ set -uo pipefail
10
+
11
+ HANDLE="${1:?ig handle required}"
12
+ ROOT="/Users/matthewdi/social-autoposter/scripts/ig_creators_run"
13
+ OUT="$ROOT/$HANDLE"
14
+ URLS="$OUT/urls.txt"
15
+ LOG="$OUT/run.log"
16
+ FAIL_STREAK_LIMIT="${FAIL_STREAK_LIMIT:-5}"
17
+ SLEEP_BETWEEN="${SLEEP_BETWEEN:-3}"
18
+
19
+ [[ -f "$URLS" ]] || { echo "ERROR: $URLS missing"; exit 2; }
20
+
21
+ DEEPGRAM_API_KEY="$(grep -E '^DEEPGRAM_API_KEY=' /Users/matthewdi/fazm/web/.env.local | head -1 | cut -d= -f2-)"
22
+ [[ -n "$DEEPGRAM_API_KEY" ]] || { echo "ERROR: no DEEPGRAM_API_KEY"; exit 1; }
23
+
24
+ TOTAL=$(wc -l < "$URLS" | tr -d ' ')
25
+ echo "[start] handle=$HANDLE total=$TOTAL out=$OUT" | tee -a "$LOG"
26
+
27
+ i=0
28
+ ok=0
29
+ skipped=0
30
+ failed=0
31
+ streak=0
32
+
33
+ while IFS= read -r URL; do
34
+ [[ -z "$URL" ]] && continue
35
+ i=$((i+1))
36
+ SHORT=$(echo "$URL" | sed -E 's|.*/(reel\|p)/([^/]+)/?.*|\2|')
37
+ MP4="$OUT/${SHORT}.mp4"
38
+ DGM="$OUT/${SHORT}.deepgram.json"
39
+ printf "[%2d/%d] %s " "$i" "$TOTAL" "$SHORT"
40
+
41
+ if [[ -f "$MP4" && -f "$DGM" ]]; then
42
+ echo "skip (already done)" | tee -a "$LOG"
43
+ skipped=$((skipped+1))
44
+ continue
45
+ fi
46
+
47
+ # download
48
+ if [[ ! -f "$MP4" ]]; then
49
+ if ! yt-dlp --cookies-from-browser chrome --no-warnings -q \
50
+ -o "$OUT/${SHORT}.%(ext)s" --write-info-json "$URL" \
51
+ >>"$LOG" 2>&1; then
52
+ echo "FAIL download" | tee -a "$LOG"
53
+ failed=$((failed+1))
54
+ streak=$((streak+1))
55
+ if (( streak >= FAIL_STREAK_LIMIT )); then
56
+ echo "[stop] $streak consecutive failures, likely rate-limited" | tee -a "$LOG"
57
+ break
58
+ fi
59
+ sleep "$SLEEP_BETWEEN"
60
+ continue
61
+ fi
62
+ fi
63
+ streak=0
64
+
65
+ # audio
66
+ AUDIO="$OUT/${SHORT}.m4a"
67
+ if [[ ! -f "$AUDIO" ]]; then
68
+ ffmpeg -y -loglevel error -i "$MP4" -vn -c:a copy "$AUDIO" 2>>"$LOG" \
69
+ || ffmpeg -y -loglevel error -i "$MP4" -vn -c:a aac -b:a 96k "$AUDIO" 2>>"$LOG" \
70
+ || { echo "FAIL audio" | tee -a "$LOG"; failed=$((failed+1)); continue; }
71
+ fi
72
+
73
+ # transcribe
74
+ if [[ ! -f "$DGM" ]]; then
75
+ HTTP=$(curl -sS -o "$DGM" -w "%{http_code}" -X POST \
76
+ -H "Authorization: Token ${DEEPGRAM_API_KEY}" \
77
+ -H "Content-Type: audio/m4a" \
78
+ --data-binary "@${AUDIO}" \
79
+ "https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&punctuate=true&detect_language=true")
80
+ if [[ "$HTTP" != "200" ]]; then
81
+ echo "FAIL deepgram http=$HTTP" | tee -a "$LOG"
82
+ failed=$((failed+1))
83
+ continue
84
+ fi
85
+ fi
86
+
87
+ DUR=$(python3 -c "import json,sys; d=json.load(open('$DGM')); print(d['metadata'].get('duration',''))" 2>/dev/null)
88
+ echo "ok dur=${DUR}s" | tee -a "$LOG"
89
+ ok=$((ok+1))
90
+ sleep "$SLEEP_BETWEEN"
91
+ done < "$URLS"
92
+
93
+ echo "[done] ok=$ok skipped=$skipped failed=$failed processed=$i/$TOTAL" | tee -a "$LOG"
@@ -0,0 +1,243 @@
1
+ #!/opt/homebrew/bin/python3.11
2
+ """
3
+ Pick the next IG post type (organic vs product) and the next pending video of
4
+ that type. Writes one JSON line to stdout for the shell harness to read.
5
+
6
+ Algorithm: inverse-recent-share weighting, identical to the Twitter pipeline's
7
+ scripts/pick_project.py. effective_weight = config_weight / (1 + posts in the
8
+ last RECENT_WINDOW_DAYS). Configured via the `instagram` block in config.json:
9
+ post_type_weights: { organic: N, product: M } # relative target shares
10
+ recent_window_days: 7 # rolling window
11
+ A type that has been posting heavily is dampened toward under-posted ones, but
12
+ never selected above its raw config weight. Settles toward the target ratio
13
+ over time.
14
+
15
+ Usage:
16
+ ig_post_type_picker.py # pick across all accounts (legacy)
17
+ ig_post_type_picker.py --account NAME # scope to one target_account
18
+
19
+ Output:
20
+ {"post_type": "organic", "video_path": "...", "post_number": 4,
21
+ "target_account": "matt_diak", "reason": "...", "fallback": false}
22
+
23
+ Exit codes:
24
+ 0 — picked successfully
25
+ 2 — no draft videos of either type (queue exhausted for the scoped account)
26
+ 3 — config error / DB error
27
+ """
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import random
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
37
+ from http_api import api_get
38
+
39
+ CONFIG_FILE = Path.home() / "social-autoposter" / "config.json"
40
+
41
+
42
+ def load_ig_config():
43
+ """Return (post_type_weights dict, recent_window_days int, product_cooldown_posts int) from config.json."""
44
+ cfg = json.loads(CONFIG_FILE.read_text())
45
+ ig = cfg.get("instagram") or {}
46
+ weights = ig.get("post_type_weights") or ig.get("post_type_ratio") or {
47
+ "organic": 4,
48
+ "product": 1,
49
+ }
50
+ days = int(ig.get("recent_window_days", 7))
51
+ # Project diversity cooldown: how many of the most recent posted rows on
52
+ # this account to look at when deciding which project_names are "recently
53
+ # posted" and therefore ineligible for the next product draft. Default 6
54
+ # means the same project cannot appear twice within any 6-post sliding
55
+ # window per account. Hard rule (no cascade relaxation): if every product
56
+ # draft is blocked, the picker falls back to organic (which has NULL
57
+ # project_name and is never on cooldown).
58
+ cooldown = int(ig.get("product_cooldown_posts", 6))
59
+ return weights, days, cooldown
60
+
61
+
62
+ def main():
63
+ ap = argparse.ArgumentParser()
64
+ ap.add_argument("--account", help="scope all queries to target_account (default: pick across all)")
65
+ args = ap.parse_args()
66
+
67
+ type_weights_cfg, window_days, cooldown_posts = load_ig_config()
68
+ account = args.account
69
+
70
+ # Single round trip: raw rows for weighting + cooldown + draft selection.
71
+ # All weighting / cooldown / fallback logic stays local (HTTP-only).
72
+ _ctx = api_get(
73
+ "/api/v1/media-posts/ig-picker-context",
74
+ query={
75
+ "target_account": account,
76
+ "window_days": window_days,
77
+ "cooldown_posts": cooldown_posts,
78
+ },
79
+ )
80
+ ctx = (_ctx.get("data") or {})
81
+ recent_counts = dict(ctx.get("recent_type_counts") or {})
82
+ recent_posted_projects = list(ctx.get("recent_posted_projects") or [])
83
+ all_drafts = list(ctx.get("drafts") or [])
84
+ for t in ("organic", "product"):
85
+ recent_counts.setdefault(t, 0)
86
+
87
+ eligible = {
88
+ t: float(type_weights_cfg.get(t, 0))
89
+ for t in ("organic", "product")
90
+ if float(type_weights_cfg.get(t, 0)) > 0
91
+ }
92
+ if not eligible:
93
+ sys.stderr.write("instagram.post_type_weights is empty in config.json\n")
94
+ sys.exit(3)
95
+
96
+ effective = {t: w / (1 + recent_counts[t]) for t, w in eligible.items()}
97
+ names = list(effective.keys())
98
+ ws = [effective[n] for n in names]
99
+ target = random.choices(names, weights=ws, k=1)[0]
100
+
101
+ def _recent_project_names(window):
102
+ # Project names appearing in the last `window` posted IG rows on this
103
+ # account (already account-scoped + NULL-excluded by the endpoint).
104
+ # Returns a set; empty when window<=0 or no account scoping.
105
+ if window <= 0 or not account:
106
+ return set()
107
+ return set(recent_posted_projects)
108
+
109
+ def _draft_query(type_, blocked_projects=None):
110
+ # First draft of this type from the endpoint's account-scoped list
111
+ # (already ordered by post_number ASC). For product drafts, exclude
112
+ # rows whose project_name is in blocked_projects (project diversity
113
+ # cooldown). NULL project_name rows always pass (organic-shaped, never
114
+ # on cooldown). Returns (post_number, video_path, project_name) or None.
115
+ for d in all_drafts:
116
+ if d.get("post_type") != type_:
117
+ continue
118
+ pn = d.get("project_name")
119
+ if blocked_projects and pn is not None and pn in blocked_projects:
120
+ continue
121
+ return (d.get("post_number"), d.get("video_path"), pn)
122
+ return None
123
+
124
+ # Project diversity cooldown applies to product drafts only. Hard rule:
125
+ # same project_name cannot appear within the last N posted rows on this
126
+ # account. If no product draft survives the filter, fall back to organic
127
+ # (which is not subject to the cooldown since its project_name is NULL).
128
+ # We do NOT relax the cooldown window, because the whole point is to
129
+ # prevent the exact same product from posting twice in a short stretch.
130
+ row = None
131
+ fallback = False
132
+ fallback_from = None
133
+ cooldown_blocked = set()
134
+ cooldown_window_used = 0
135
+ cooldown_skipped_drafts = []
136
+
137
+ def _pick_product_with_cooldown():
138
+ nonlocal cooldown_blocked, cooldown_window_used, cooldown_skipped_drafts
139
+ cooldown_blocked = _recent_project_names(cooldown_posts) if cooldown_posts > 0 else set()
140
+ cooldown_window_used = cooldown_posts
141
+ if cooldown_blocked:
142
+ # Diagnostic: which product drafts got filtered out by the cooldown
143
+ # (computed locally from the endpoint's draft list).
144
+ cooldown_skipped_drafts = [
145
+ (d.get("post_number"), d.get("project_name"))
146
+ for d in all_drafts
147
+ if d.get("post_type") == "product" and d.get("project_name") in cooldown_blocked
148
+ ]
149
+ return _draft_query("product", blocked_projects=cooldown_blocked)
150
+
151
+ if target == "product":
152
+ row = _pick_product_with_cooldown()
153
+ else:
154
+ row = _draft_query(target)
155
+
156
+ if row is None:
157
+ # Fall back to the other type if this one has no drafts (either truly
158
+ # empty for organic, or all-cooldown-blocked for product). For
159
+ # product->organic fallback this preserves cadence without weakening
160
+ # the cooldown.
161
+ other = "product" if target == "organic" else "organic"
162
+ # ...but ONLY if the other type is still eligible (config weight > 0).
163
+ # A type explicitly disabled via post_type_weights=0 (e.g. product
164
+ # paused during an IG link-sharing restriction) must NEVER be posted
165
+ # through the empty-queue fallback, otherwise weight=0 is not a real
166
+ # off switch. If the only eligible type's queue is empty, exit 2
167
+ # (nothing to post this fire) rather than leak the disabled type.
168
+ if other not in eligible:
169
+ sys.stderr.write(
170
+ f"queue empty for eligible type '{target}'; fallback type "
171
+ f"'{other}' is disabled (config_weights={type_weights_cfg}), "
172
+ "not leaking it"
173
+ + (f" (target_account={account})" if account else "")
174
+ + "\n"
175
+ )
176
+ sys.exit(2)
177
+ if other == "product":
178
+ row = _pick_product_with_cooldown()
179
+ else:
180
+ row = _draft_query(other)
181
+ if row is None:
182
+ sys.stderr.write(
183
+ "queue empty: no draft rows for either organic or product"
184
+ + (f" (target_account={account})" if account else "")
185
+ + (
186
+ f" (cooldown blocked product drafts: {cooldown_skipped_drafts})"
187
+ if cooldown_skipped_drafts else ""
188
+ )
189
+ + "\n"
190
+ )
191
+ sys.exit(2)
192
+ sys.stderr.write(
193
+ f"queue imbalance: target={target} has 0 drafts"
194
+ + (
195
+ f" (cooldown blocked product drafts: {cooldown_skipped_drafts})"
196
+ if target == "product" and cooldown_skipped_drafts else ""
197
+ )
198
+ + f", falling back to {other}"
199
+ + (f" (target_account={account})" if account else "")
200
+ + "\n"
201
+ )
202
+ fallback_from = target
203
+ target = other
204
+ fallback = True
205
+
206
+ post_number, video_path, project_name = row
207
+
208
+ if target == "product":
209
+ sys.stderr.write(
210
+ f"[ig_picker] cooldown account={account or '<global>'} "
211
+ f"window={cooldown_window_used} "
212
+ f"blocked={sorted(cooldown_blocked) or '[]'} "
213
+ f"chose=project_name={project_name} post_number={post_number}\n"
214
+ )
215
+
216
+ out = {
217
+ "post_type": target,
218
+ "video_path": video_path,
219
+ "post_number": post_number,
220
+ "target_account": account,
221
+ "project_name": project_name,
222
+ "reason": (
223
+ f"window={window_days}d account={account or '<global>'} "
224
+ f"recent={recent_counts} config_weights={type_weights_cfg} "
225
+ f"effective={effective} chose={target}"
226
+ + (f" (fallback_from={fallback_from})" if fallback else "")
227
+ + (
228
+ f" cooldown_window={cooldown_window_used}"
229
+ f" cooldown_blocked={sorted(cooldown_blocked)}"
230
+ + (f" cooldown_skipped_drafts={cooldown_skipped_drafts}" if cooldown_skipped_drafts else "")
231
+ if target == "product" else ""
232
+ )
233
+ ),
234
+ "fallback": fallback,
235
+ "cooldown_window": cooldown_window_used if target == "product" else None,
236
+ "cooldown_blocked_projects": sorted(cooldown_blocked) if target == "product" else [],
237
+ "cooldown_skipped_drafts": cooldown_skipped_drafts if target == "product" else [],
238
+ }
239
+ print(json.dumps(out))
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # End-to-end: download latest IG post from a creator, transcribe with Deepgram.
3
+ # Usage: ./ig_scrape_transcribe.sh <ig_handle> [N_CANDIDATES=3]
4
+ #
5
+ # How it works:
6
+ # 1. Loads DEEPGRAM_API_KEY from ~/fazm/web/.env.local (sibling repo).
7
+ # 2. Asks the user's logged-in Chrome (via playwright-extension MCP)
8
+ # for the first N reel/post URLs on the profile grid. (Caller must
9
+ # pass the URLs in via stdin, one per line — see ig_pick_latest.py
10
+ # for the picker that does this end-to-end via MCP.)
11
+ # 3. yt-dlp fetches metadata for each candidate, sorts by upload_date,
12
+ # keeps the most recent (this skips pinned-but-old posts).
13
+ # 4. Downloads the chosen post, extracts audio, sends to Deepgram nova-3.
14
+ #
15
+ # Standalone fallback: if no URLs on stdin, pass a single post URL as $2.
16
+ set -euo pipefail
17
+
18
+ HANDLE="${1:?ig handle required}"
19
+ SINGLE_URL="${2:-}"
20
+ OUT_DIR="/tmp/ig_scrape/${HANDLE}"
21
+ mkdir -p "$OUT_DIR"
22
+
23
+ DEEPGRAM_API_KEY="$(grep -E '^DEEPGRAM_API_KEY=' /Users/matthewdi/fazm/web/.env.local | head -1 | cut -d= -f2-)"
24
+ [[ -z "${DEEPGRAM_API_KEY}" ]] && { echo "ERROR: no DEEPGRAM_API_KEY"; exit 1; }
25
+ echo "[1/5] Deepgram key loaded (len=${#DEEPGRAM_API_KEY})"
26
+
27
+ # Collect candidate URLs
28
+ if [[ -n "$SINGLE_URL" ]]; then
29
+ CANDIDATES=("$SINGLE_URL")
30
+ elif [[ ! -t 0 ]]; then
31
+ mapfile -t CANDIDATES < <(grep -E 'instagram\.com/.*/(p|reel)/' || true)
32
+ else
33
+ echo "ERROR: pass post URL as \$2 OR pipe candidate URLs on stdin" >&2
34
+ exit 2
35
+ fi
36
+ echo "[2/5] ${#CANDIDATES[@]} candidate URL(s)"
37
+
38
+ # Sort candidates by upload_date desc using yt-dlp metadata only
39
+ BEST_URL=""
40
+ BEST_DATE=""
41
+ for U in "${CANDIDATES[@]}"; do
42
+ D=$(yt-dlp --cookies-from-browser chrome --no-warnings -q --skip-download \
43
+ --print "%(upload_date)s" "$U" 2>/dev/null || true)
44
+ echo " $D $U"
45
+ if [[ -n "$D" && "$D" > "${BEST_DATE:-}" ]]; then
46
+ BEST_DATE="$D"
47
+ BEST_URL="$U"
48
+ fi
49
+ done
50
+ [[ -z "$BEST_URL" ]] && { echo "ERROR: no usable candidate"; exit 3; }
51
+ echo "[3/5] picked $BEST_URL (upload_date=$BEST_DATE)"
52
+
53
+ # Download the chosen post
54
+ yt-dlp --cookies-from-browser chrome --no-warnings -q \
55
+ -o "${OUT_DIR}/%(id)s.%(ext)s" --write-info-json "$BEST_URL"
56
+ VIDEO="$(ls -t "$OUT_DIR"/*.mp4 | head -1)"
57
+ echo " file: $VIDEO ($(du -h "$VIDEO" | cut -f1))"
58
+
59
+ # Extract audio
60
+ AUDIO="${VIDEO%.mp4}.m4a"
61
+ echo "[4/5] extracting audio"
62
+ ffmpeg -y -loglevel error -i "$VIDEO" -vn -c:a copy "$AUDIO" 2>/dev/null || \
63
+ ffmpeg -y -loglevel error -i "$VIDEO" -vn -c:a aac -b:a 96k "$AUDIO"
64
+
65
+ # Transcribe
66
+ echo "[5/5] Deepgram nova-3"
67
+ TJ="${VIDEO%.mp4}.deepgram.json"
68
+ curl -sS -X POST \
69
+ -H "Authorization: Token ${DEEPGRAM_API_KEY}" \
70
+ -H "Content-Type: audio/m4a" \
71
+ --data-binary "@${AUDIO}" \
72
+ "https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&punctuate=true&detect_language=true" \
73
+ -o "$TJ"
74
+
75
+ python3 - <<PY
76
+ import json, os
77
+ d = json.load(open("$TJ"))
78
+ info = json.load(open(os.path.splitext("$VIDEO")[0] + ".info.json"))
79
+ ch = d["results"]["channels"][0]
80
+ print()
81
+ print("==================== RESULT ====================")
82
+ print(f"creator : @${HANDLE}")
83
+ print(f"url : {info.get('webpage_url') or info.get('original_url')}")
84
+ print(f"upload_dt : {info.get('upload_date')}")
85
+ print(f"duration : {d['metadata'].get('duration')}s")
86
+ print(f"language : {ch.get('detected_language','?')}")
87
+ print(f"caption : {(info.get('description') or '')[:240]}")
88
+ print("------------------- TRANSCRIPT ------------------")
89
+ print(ch['alternatives'][0]['transcript'] or '(no speech detected)')
90
+ print("=================================================")
91
+ PY